#!/usr/bin/env ruby
require 'rubygems'
begin
  require 'linkeddata'
rescue LoadError
end
$:.unshift(File.expand_path("../../lib", __FILE__))
require 'json/ld'
require 'getoptlong'
require 'open-uri'
require 'logger'

def run(input, options, parser_options)
  reader_class = RDF::Reader.for(options[:input_format].to_sym)
  raise "Reader not found for #{options[:input_format]}" unless reader_class

  # Override default (or specified) output format when framing
  options[:format] = :jsonld if options[:compact] || options[:frame]

  # If input format is not JSON-LD, transform input to JSON-LD first
  reader = if options[:input_format] != :jsonld
    reader_class.new(input, parser_options)
  end

  start = Time.new
  if options[:expand]
    parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context)
    input = JSON::LD::API.fromRdf(reader) if reader
    output = JSON::LD::API.expand(input, **parser_options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    STDERR.puts "Expanded in #{secs} seconds." unless options[:quiet]
  elsif options[:compact]
    input = JSON::LD::API.fromRdf(reader) if reader
    output = JSON::LD::API.compact(input, parser_options[:context], **parser_options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    STDERR.puts "Compacted in #{secs} seconds." unless options[:quiet]
  elsif options[:flatten]
    input = JSON::LD::API.fromRdf(reader) if reader
    output = JSON::LD::API.flatten(input, parser_options[:context], **parser_options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    STDERR.puts "Flattened in #{secs} seconds." unless options[:quiet]
  elsif options[:frame]
    input = JSON::LD::API.fromRdf(reader) if reader
    output = JSON::LD::API.frame(input, parser_options[:frame], **parser_options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    STDERR.puts "Framed in #{secs} seconds." unless options[:quiet]
  else
    parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context)
    parser_options[:standard_prefixes] = true
    reader ||= JSON::LD::Reader.new(input, **parser_options)
    num = 0
    RDF::Writer.for(options[:output_format]).new(options[:output], **parser_options) do |w|
      reader.each do |statement|
        num += 1
        w << statement
      end
    end
    secs = Time.new - start
    STDERR.puts "\nParsed #{num} statements in #{secs} seconds @ #{num/secs} statements/second." unless options[:quiet]
  end
rescue
  fname = case
  when input.respond_to?(:path) then input.path
  when input.respond_to?(:requested_url) && input.requested_url then input.requested_url
  when input.respond_to?(:base_uri) then input.base_uri
  else  "-stdin-"
  end
  STDERR.puts("Error in #{fname}")
  raise
end

logger = Logger.new(STDERR)
logger.level = Logger::WARN
logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity}: #{msg}\n"}

parser_options = {
  base:     nil,
  validate: false,
  stream:   false,
  strict:   false,
  logger:   logger,
}

options = {
  output:         STDOUT,
  output_format:  :jsonld,
  input_format:   :jsonld,
  logger:         logger,
}
input = nil

OPT_ARGS = [
  ["--debug",         GetoptLong::NO_ARGUMENT,      "Turn on verbose debugging"],
  ["--compact",       GetoptLong::NO_ARGUMENT,      "Compact document, using --context"],
  ["--compactArrays", GetoptLong::OPTIONAL_ARGUMENT, "Set compactArrays option"],
  ["--context",       GetoptLong::REQUIRED_ARGUMENT,"Context to apply for expand, compact and converting from RDF"],
  ["--embed",         GetoptLong::REQUIRED_ARGUMENT,"a flag specifying that objects should be directly embedded in the output, instead of being referred to by their IRI"],
  ["--evaluate","-e", GetoptLong::REQUIRED_ARGUMENT,"Evaluate argument as a JSON-LD document"],
  ["--expand",        GetoptLong::NO_ARGUMENT,      "Expand document, using an optional --context"],
  ["--expanded",      GetoptLong::OPTIONAL_ARGUMENT, "Input is already expanded"],
  ["--explicit",      GetoptLong::OPTIONAL_ARGUMENT, "a flag specifying that for properties to be included in the output, they must be explicitly declared in the framing context"],
  ["--flatten",       GetoptLong::NO_ARGUMENT,      "Flatten document, using an optional --context"],
  ["--format",        GetoptLong::REQUIRED_ARGUMENT,"Specify output format when converting to RDF"],
  ["--frame",         GetoptLong::REQUIRED_ARGUMENT,"Frame document, using the file or URL as a frame specification"],
  ["--input-format",  GetoptLong::REQUIRED_ARGUMENT,"Format of the input document, when converting from RDF."],
  ["--omitDefault",   GetoptLong::OPTIONAL_ARGUMENT,"a flag specifying that properties that are missing from the JSON-LD input should be omitted from the output"],
  ["--output", "-o",  GetoptLong::REQUIRED_ARGUMENT,"Output to the specified file path"],
  ["--parse-only",    GetoptLong::NO_ARGUMENT,      "Parse the document for well-formedness only"],
  ["--processingMode",GetoptLong::REQUIRED_ARGUMENT,"Set processing mode, defaults to json-ld-1.1"],
  ["--quiet",         GetoptLong::NO_ARGUMENT,      "Supress most output other than progress indicators"],
  ["--rename_bnodes", GetoptLong::OPTIONAL_ARGUMENT,"Rename bnodes as part of expansion, or keep them the same"],
  ["--rdfstar",       GetoptLong::NO_ARGUMENT,      "Parse JSON-LD-star"],
  ["--requireAll",    GetoptLong::OPTIONAL_ARGUMENT,"Rename bnodes as part of expansion, or keep them the same"],
  ["--stream",        GetoptLong::NO_ARGUMENT,      "Use Streaming reader/writer"],
  ["--unique_bnodes", GetoptLong::OPTIONAL_ARGUMENT,"Use unique bnode identifiers"],
  ["--uri",           GetoptLong::REQUIRED_ARGUMENT,"URI to be used as the document base"],
  ["--validate",      GetoptLong::NO_ARGUMENT,      "Validate while processing"],
  ["--help", "-?",    GetoptLong::NO_ARGUMENT,      "This message"]
]
def usage
  STDERR.puts %{Usage: #{$0} [options] file ...}
  width = OPT_ARGS.map do |o|
    l = o.first.length
    l += o[1].length + 2 if o[1].is_a?(String)
    l
  end.max
  OPT_ARGS.each do |o|
    s = "  %-*s  " % [width, (o[1].is_a?(String) ? "#{o[0,2].join(', ')}" : o[0])]
    s += o.last
    STDERR.puts s
  end
  exit(1)
end

opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]})

opts.each do |opt, arg|
  case opt
  when '--debug'          then logger.level = Logger::DEBUG
  when '--compact'        then options[:compact] = true
  when "--compactArrays"  then parser_options[:compactArrays] = (arg || 'true') == 'true'
  when '--context'        then parser_options[:context] = RDF::URI(arg).absolute? ? arg : File.open(arg)
  when '--evaluate'       then input = arg
  when '--expand'         then options[:expand] = true
  when "--expanded"       then parser_options[:expanded] = (arg || 'true') == 'true'
  when "--explicit"       then parser_options[:compactArrays] = (arg || 'true') == 'true'
  when '--format'         then options[:output_format] = arg.to_sym
  when '--flatten'        then options[:flatten] = arg
  when '--frame'          then options[:frame] = parser_otpions[:frame] = RDF::URI(arg).absolute? ? arg : File.open(arg)
  when '--input-format'   then options[:input_format] = arg.to_sym
  when "--omitDefault"    then parser_options[:omitDefault] = (arg || 'true') == 'true'
  when '--output'         then options[:output] = File.open(arg, "w")
  when '--parse-only'     then options[:parse_only] = true
  when '--processingMode' then parser_options[:processingMode] = arg
  when '--quiet'
    options[:quiet] = true
    logger.level = Logger::FATAL
  when "--rdfstar"        then parser_options[:rdfstar] = true
  when "--rename_bnodes"  then parser_options[:rename_bnodes] = (arg || 'true') == 'true'
  when "--requireAll"     then parser_options[:requireAll] = (arg || 'true') == 'true'
  when '--stream'         then parser_options[:stream] = true
  when "--unique_bnodes"  then parser_options[:unique_bnodes] = (arg || 'true') == 'true'
  when '--uri'            then parser_options[:base] = arg
  when '--validate'       then parser_options[:validate] = true
  when '--help'           then usage
  when '--embed'
    case arg
    when '@always', '@never', '@link', '@once'
      parser_options[:embed] = arg
    when 'true'
      parser_options[:embed] = '@never'
    when 'false'
      parser_options[:embed] = '@first'
    else
      STDERR.puts "--embed option takes one of @always, @never, @link, or @once"
      exit(1)
    end
  end
end

# Hack
if !(options.keys & %i{expand compact flatten frame}).empty? &&
   (parser_options[:stream] || options[:output_format] != :jsonld)
   STDERR.puts "Incompatible options"
   exit(1)
end

if ARGV.empty?
  s = input ? input : $stdin.read
  run(StringIO.new(s), options, parser_options)
else
  ARGV.each do |file|
    # Call with opened files
    RDF::Util::File.open_file(file, **options) {|f| run(f, options, parser_options)}
  end
end
puts
