require_relative '../parsers/serializer'

module BELParser
  module Script
    # NanopubMapper maps BEL Script AST nodes and state to aggregated
    # nanopub hash objects.
    class NanopubMapper
      include BELParser::Parsers

      STATEMENT_TYPES = [
        :simple_statement,
        :nested_statement,
        :observed_term
      ]

      DEFINITIONS = [:annotation_definitions, :namespace_definitions]

      def initialize(ast_enum, omit_on_error = false, omit_on_warning = false)
        @ast_enum        = ast_enum
        @omit_on_error   = omit_on_error
        @omit_on_warning = omit_on_warning
      end

      def each
        if block_given?
          @ast_enum.each do |(num, line, ast_node, state)|
            next unless STATEMENT_TYPES.include?(ast_node.type)

            errors   = errors(ast_node)
            warnings = warnings(ast_node)

            if (@omit_on_error && !errors.empty?) ||
              (@omit_on_warning && !warnings.empty?)

              report(num, line, errors, warnings)
              next
            end

            yield [num, line, ast_node, nanopub(ast_node, state)]
          end
        else
          enum_for(:each)
        end
      end

      def nanopub(ast_node, state)
        {
          bel_statement:      serialize(ast_node),
          citation:           citation(state[:citation]),
          support:            support(state[:support]),
          experiment_context: experiment_context(state[:annotations]),
          references:         references(*state.values_at(*DEFINITIONS)),
          metadata:           {
            bel_version:     state[:specification].version,
            document_header: state[:document_properties] || nil
          }
        }
      end

      def report(num, line, errors, warnings)
        warn "Line #{num}: #{line}"
        errors.each do |err|
          warn "  #{err}"
        end
        warnings.each do |warn|
          warn "  #{warn}"
        end
        warn
      end

      def errors(ast_node)
        ast_node
          .syntax_errors
          .select do |err|
            err.is_a?(::BELParser::Language::Syntax::SyntaxError)
          end.each(&:to_s)
      end

      def warnings(ast_node)
        ast_node
          .syntax_errors
          .select do |warn|
            warn.is_a?(::BELParser::Language::Syntax::SyntaxError) ||
              warn.is_a?(::BELParser::Language::Semantics::SemanticsWarning)
          end.each(&:to_s)
      end

      def citation(citation)
        return nil unless citation
        citation.each do |field, value|
          citation[field] = value
        end
      end

      def support(support)
        support
      end

      def experiment_context(annotations)
        (annotations || []).map do |name, value|
          {
            name:  name,
            value: value
          }
        end
      end

      def references(anno_defs, ns_defs)
        {
          annotations: (anno_defs || []).map do |keyword, (type, domain)|
            {
              keyword: keyword,
              type:    type,
              domain:  domain_value(type, domain)
            }
          end,
          namespaces: (ns_defs || []).map do |keyword, namespace|
            {
              keyword: keyword,
              type:    namespace.uri? ? :uri : :url,
              domain:  namespace.uri? ? namespace.uri : namespace.url
            }
          end
        }
      end

      def domain_value(type, domain)
        case type
        when :url, :uri
          domain.identifier
        when :list
          domain
        else
          domain.to_s
        end
      end
    end
  end
end

if __FILE__ == $PROGRAM_NAME
  $LOAD_PATH.unshift(
    File.join(File.expand_path(File.dirname(__FILE__)), '..', '..', '..', 'lib'))

  require 'json'
  require 'bel_parser'
  require 'bel_parser/language'
  require 'bel_parser/parsers/serializer'
  require 'bel_parser/script'
  require 'bel_parser/resource/resource_url_reader'
  include BELParser::Script

  uri_reader = BELParser::Resource.default_uri_reader
  url_reader = BELParser::Resource::ResourceURLReader.new(true)

  namespaces = Hash[
    ARGV.map do |ns|
      prefix, identifier = ns.split('=')
      dataset            = uri_reader.retrieve_resource(identifier)
      dataset ? [prefix, dataset] : nil
    end.compact
  ]

  initial_state = {
    uri_reader:            uri_reader,
    url_reader:            url_reader,
    specification:         BELParser::Language.specification('2.0'),
    namespace_definitions: namespaces
  }

  io =
    if ARGV.first
      File.open(ARGV.first)
    else
      $stdin
    end

  nanopubs =
    NanopubMapper.new(
      Validator.new(
        StateAggregator.new(
          FirstNode.new(Filter.new(BELParser::ASTGenerator.new(io))),
          initial_state))).each.map(&:last).to_a
  puts JSON.dump(nanopubs)
end
