# frozen_string_literal: true

require_relative '../../../puppet/parser/functions'
require_relative '../../../puppet/parser/files'
require_relative '../../../puppet/resource/type_collection'
require_relative '../../../puppet/resource/type'
require 'monitor'

module Puppet::Pops
module Parser
# Supporting logic for the parser.
# This supporting logic has slightly different responsibilities compared to the original Puppet::Parser::Parser.
# It is only concerned with parsing.
#
class Parser
  # Note that the name of the contained class and the file name (currently parser_support.rb)
  # needs to be different as the class is generated by Racc, and this file (parser_support.rb) is included as a mix in
  #

  # Simplify access to the Model factory
  # Note that the parser/parser support does not have direct knowledge about the Model.
  # All model construction/manipulation is made by the Factory.
  #
  Factory = Model::Factory

  attr_accessor :lexer
  attr_reader :definitions

  # Returns the token text of the given lexer token, or nil, if token is nil
  def token_text t
    return t if t.nil?

    if t.is_a?(Factory) && t.model_class <= Model::QualifiedName
      t['value']
    elsif t.is_a?(Model::QualifiedName)
      t.value
    else
      # else it is a lexer token
      t[:value]
    end
  end

  # Produces the fully qualified name, with the full (current) namespace for a given name.
  #
  # This is needed because class bodies are lazily evaluated and an inner class' container(s) may not
  # have been evaluated before some external reference is made to the inner class; its must therefore know its complete name
  # before evaluation-time.
  #
  def classname(name)
    [namespace, name].join('::').sub(/^::/, '')
  end

  # Raises a Parse error with location information. Information about file is always obtained from the
  # lexer. Line and position is produced if the given semantic is a Positioned object and have been given an offset.
  #
  def error(semantic, message)
    except = Puppet::ParseError.new(message)
    if semantic.is_a?(LexerSupport::TokenValue)
      except.file = semantic[:file];
      except.line = semantic[:line];
      except.pos = semantic[:pos];
    else
      locator = @lexer.locator
      except.file = locator.file
      if semantic.is_a?(Factory)
        offset = semantic['offset']
        unless offset.nil?
          except.line = locator.line_for_offset(offset)
          except.pos = locator.pos_on_line(offset)
        end
      end
    end
    raise except
  end

  # Parses a file expected to contain pp DSL logic.
  def parse_file(file)
    unless Puppet::FileSystem.exist?(file)
      unless file =~ /\.pp$/
        file += ".pp"
      end
    end
    @lexer.file = file
    _parse
  end

  def initialize
    @lexer = Lexer2.new
    @namestack = []
    @definitions = []
  end

  # This is a callback from the generated parser (when an error occurs while parsing)
  #
  def on_error(token, value, stack)
    if token == 0 # denotes end of file
      value_at = 'end of input'
    else
      value_at = "'#{value[:value]}'"
    end
    error = Issues::SYNTAX_ERROR.format(:where => value_at)
    error = "#{error}, token: #{token}" if @yydebug

    # Note, old parser had processing of "expected token here" - do not try to reinstate:
    # The 'expected' is only of value at end of input, otherwise any parse error involving a
    # start of a pair will be reported as expecting the close of the pair - e.g. "$x.each |$x {|", would
    # report that "seeing the '{', the '}' is expected. That would be wrong.
    # Real "expected" tokens are very difficult to compute (would require parsing of racc output data). Output of the stack
    # could help, but can require extensive backtracking and produce many options.
    #
    # The lexer should handle the "expected instead of end of file for strings, and interpolation", other expectancies
    # must be handled by the grammar. The lexer may have enqueued tokens far ahead - the lexer's opinion about this
    # is not trustworthy.
    #
    file = nil
    line = nil
    pos  = nil
    if token != 0
      file = value[:file]
      locator = value.locator
      if locator.is_a?(Puppet::Pops::Parser::Locator::SubLocator)
        # The error occurs when doing sub-parsing and the token must be transformed
        # Transpose the local offset, length to global "coordinates"
        global_offset, _ = locator.to_global(value.offset, value.length)
        line = locator.locator.line_for_offset(global_offset)
        pos = locator.locator.pos_on_line(global_offset)
      else
        line = value[:line]
        pos  = value[:pos]
      end
    else
      # At end of input, use what the lexer thinks is the source file
      file = lexer.file
    end
    file = nil unless file.is_a?(String) && !file.empty?
    raise Puppet::ParseErrorWithIssue.new(error, file, line, pos, nil, Issues::SYNTAX_ERROR.issue_code)
  end

  # Parses a String of pp DSL code.
  #
  def parse_string(code, path = nil)
    @lexer.lex_string(code, path)
    _parse()
  end

  # Mark the factory wrapped model object with location information
  # @return [Factory] the given factory
  # @api private
  #
  def loc(factory, start_locatable, end_locatable = nil)
    factory.record_position(@lexer.locator, start_locatable, end_locatable)
  end

  # Mark the factory wrapped heredoc model object with location information
  # @return [Factory] the given factory
  # @api private
  #
  def heredoc_loc(factory, start_locatable, end_locatable = nil)
    factory.record_heredoc_position(start_locatable, end_locatable)
  end

  def aryfy(o)
    o = [o] unless o.is_a?(Array)
    o
  end

  def namespace
    @namestack.join('::')
  end

  def namestack(name)
    @namestack << name
  end

  def namepop
    @namestack.pop
  end

  def add_definition(definition)
    @definitions << definition.model
    definition
  end

  # Transforms an array of expressions containing literal name expressions to calls if followed by an
  # expression, or expression list
  #
  def transform_calls(expressions)
    # Factory transform raises an error if a non qualified name is followed by an argument list
    # since there is no way that that can be transformed back to sanity. This occurs in situations like this:
    #
    #  $a = 10, notice hello
    #
    # where the "10, notice" forms an argument list. The parser builds an Array with the expressions and includes
    # the comma tokens to enable the error to be reported against the first comma.
    #

    Factory.transform_calls(expressions)
  rescue Factory::ArgsToNonCallError => e
    # e.args[1] is the first comma token in the list
    # e.name_expr is the function name expression
    if e.name_expr.is_a?(Factory) && e.name_expr.model_class <= Model::QualifiedName
      error(e.args[1], _("attempt to pass argument list to the function '%{name}' which cannot be called without parentheses") % { name: e.name_expr['value'] })
    else
      error(e.args[1], _("illegal comma separated argument list"))
    end
  end

  # Transforms a LEFT followed by the result of attribute_operations, this may be a call or an invalid sequence
  def transform_resource_wo_title(left, resource, lbrace_token, rbrace_token)
    Factory.transform_resource_wo_title(left, resource, lbrace_token, rbrace_token)
  end

  # Creates a program with the given body.
  #
  def create_program(body)
    locator = @lexer.locator
    Factory.PROGRAM(body, definitions, locator)
  end

  # Creates an empty program with a single No-op at the input's EOF offset with 0 length.
  #
  def create_empty_program
    locator = @lexer.locator
    no_op = Factory.literal(nil)
    # Create a synthetic NOOP token at EOF offset with 0 size. The lexer does not produce an EOF token that is
    # visible to the grammar rules. Creating this token is mainly to reuse the positioning logic as it
    # expects a token decorated with location information.
    _, token = @lexer.emit_completed([:NOOP, '', 0], locator.string.bytesize)
    loc(no_op, token)
    # Program with a Noop
    Factory.PROGRAM(no_op, [], locator)
  end

  # Performs the parsing and returns the resulting model.
  # The lexer holds state, and this is setup with {#parse_string}, or {#parse_file}.
  #
  # @api private
  #
  def _parse
    begin
      @yydebug = false
      main = yyparse(@lexer, :scan)
    end
    main
  ensure
    @lexer.clear
    @namestack = []
    @definitions = []
  end
end
end
end
