require 'singleton'
require 'set'

# A singleton class storing all the information about the manifest being
# analysed.
#
# @api public
class PuppetLint::Data
  include Singleton

  class << self
    # Get/Set the full expanded path to the manifest file being checked.
    #
    # @api private
    attr_reader :path, :fullpath, :filename

    # Get/Set the raw manifest data, split by \n.
    #
    # @api private
    attr_accessor :manifest_lines

    # Store the tokenised manifest.
    #
    # @param [Array[PuppetLint::Lexer::Token]] tokens The Array of PuppetLint::Lexer::Token objects to store.
    # @return [nil]
    #
    # @api private
    def tokens=(tokens)
      @tokens = tokens
      @title_tokens = nil
      @resource_indexes = nil
      @class_indexes = nil
      @defined_type_indexes = nil
      @node_indexes = nil
      @function_indexes = nil
      @array_indexes = nil
      @hash_indexes = nil
      @defaults_indexes = nil
    end

    # Get the tokenised manifest.
    #
    # @param duplicate [Boolean] if true, returns a duplicate of the token array.
    # @return [Array[PuppetLint::Lexer::Token]]
    #
    # @api public
    def tokens(duplicate: false)
      duplicate ? @tokens.dup : @tokens
    end

    # Add new token.
    #
    # @param [Integer] index
    # @param [PuppetLint::Lexer::Token] token
    #
    # @api public
    def insert(index, token)
      current_token = tokens[index - 1]
      token.next_token = current_token.next_token
      token.prev_token = current_token

      current_token.next_token.prev_token = token unless current_token.next_token.nil?

      unless formatting_tokens.include?(token.type)
        current_token.next_token.prev_code_token = token unless current_token.next_token.nil?
        next_nf_idx = tokens[index..].index { |r| !formatting_tokens.include?(r.type) }
        unless next_nf_idx.nil?
          next_nf_token = tokens[index + next_nf_idx]
          token.next_code_token = next_nf_token
          next_nf_token.prev_code_token = token
        end
      end

      if formatting_tokens.include?(current_token.type)
        prev_nf_idx = tokens[0..index - 1].rindex { |r| !formatting_tokens.include?(r.type) }
        unless prev_nf_idx.nil?
          prev_nf_token = tokens[prev_nf_idx]
          token.prev_code_token = prev_nf_token
          prev_nf_token.next_code_token = token
        end
      else
        token.prev_code_token = current_token
      end

      current_token.next_token = token
      current_token.next_code_token = token unless formatting_tokens.include?(token.type)

      tokens.insert(index, token)
    end

    # Remove a token
    #
    # @param [PuppetLint::Lexer::Token] token
    #
    # @api public
    def delete(token)
      token.next_token.prev_token = token.prev_token unless token.next_token.nil?
      token.prev_token.next_token = token.next_token unless token.prev_token.nil?
      unless formatting_tokens.include?(token.type)
        token.prev_code_token.next_code_token = token.next_code_token unless token.prev_code_token.nil?
        token.next_code_token.prev_code_token = token.prev_code_token unless token.next_code_token.nil?
      end

      tokens.delete(token)
    end

    # Store the path to the manifest file and populate fullpath and filename.
    #
    # @param [String] val The path to the file
    #
    # @return [nil]
    #
    # @api private
    def path=(val)
      @path = val
      if val.nil?
        @fullpath = nil
        @filename = nil
      else
        @fullpath = File.expand_path(val, ENV.fetch('PWD', nil))
        @filename = File.basename(val)
      end
    end

    # Retrieve a list of tokens that represent resource titles
    #
    # @return [Array[PuppetLint::Lexer::Token]]
    #
    # @api private
    def title_tokens
      @title_tokens ||= begin
        result = []
        tokens.each_index do |token_idx|
          if tokens[token_idx].type == :COLON
            # gather a list of tokens that are resource titles
            if tokens[token_idx - 1].type == :RBRACK
              array_start_idx = tokens.rindex do |r|
                r.type == :LBRACK
              end
              title_array_tokens = tokens[(array_start_idx + 1)..(token_idx - 2)]
              result += title_array_tokens.select do |token|
                { STRING: true, NAME: true }.include?(token.type)
              end
            else
              next_token = tokens[token_idx].next_code_token
              result << tokens[token_idx - 1] unless next_token.type == :LBRACE
            end
          end
        end
        result
      end
    end

    # Determine if the given token contains a CLASSREF in the token chain..
    #
    # @param [PuppetLint::Lexer::Token] token
    #
    # @api private
    def classref?(token)
      current_token = token
      while (current_token = current_token.prev_code_token)
        return true if current_token.type == :CLASSREF
        return false if current_token.type == :NAME
      end
    end

    # Calculate the positions of all resource declarations within the
    # tokenised manifest. These positions only point to the content of the
    # resource declarations, they do not include resource types or titles.
    #
    # @return [Array[Hash[Symbol, Integer]]] each hash contains :start and
    #   :end pointing to the first and last {PuppetLint::Lexer::Token} of a
    #   resource declaration
    #
    # @api private
    def resource_indexes
      @resource_indexes ||= begin
        marker = 0
        result = []
        tokens.select { |t| t.type == :COLON }.each do |colon_token|
          next unless colon_token.next_code_token && colon_token.next_code_token.type != :LBRACE
          next if classref?(colon_token)

          rel_start_idx = tokens[marker..].index(colon_token)
          break if rel_start_idx.nil?

          start_idx = rel_start_idx + marker
          end_token = colon_token.next_token_of([:SEMIC, :RBRACE])
          rel_end_idx = tokens[start_idx..].index(end_token)
          raise PuppetLint::SyntaxError, colon_token if rel_end_idx.nil?

          marker = rel_end_idx + start_idx

          result << {
            start: start_idx + 1,
            end: marker,
            tokens: tokens[start_idx..marker],
            type: find_resource_type_token(start_idx),
            param_tokens: find_resource_param_tokens(tokens[start_idx..marker])
          }
        end
        result
      end
    end

    # Find the Token representing the type of a resource definition.
    #
    # @param [Integer] index
    #   The Integer pointing to the start of the resource in the `tokens`
    #   array.
    #
    # @return [PuppetLint::Lexer::Token]
    #
    # @api private
    def find_resource_type_token(index)
      lbrace_idx = tokens[0..index].rindex do |token|
        token.type == :LBRACE && token.prev_code_token.type != :QMARK
      end

      raise PuppetLint::SyntaxError, tokens[index] if lbrace_idx.nil?

      tokens[lbrace_idx].prev_code_token
    end

    # Find all the Token objects representing the parameter names in
    # a resource definition.
    #
    # @param [Array[PuppetLint::Lexer::Token]] resource_tokens
    #   An Array of Token objects that comprise the resource definition.
    #
    # @return [Array[PuppetLint::Lexer::Token]]
    #
    # @api private
    def find_resource_param_tokens(resource_tokens)
      param_tokens = []

      iter_token = resource_tokens.first.prev_token

      until iter_token.nil?
        iter_token = iter_token.next_token_of(:NAME)

        break unless resource_tokens.include?(iter_token)

        param_tokens << iter_token if iter_token && iter_token.next_code_token.type == :FARROW
      end

      param_tokens
    end

    # Calculate the positions of all class definitions within the `tokens`
    # Array.
    #
    # Returns an Array of Hashes, each containing:
    #   :start  - An Integer position in the `tokens` Array pointing to the
    #             first Token of a class definition.
    #   :end    - An Integer position in the `tokens` Array pointing to the last
    #             Token of a class definition.
    #   :tokens - An Array consisting of all the Token objects that make up the
    #             class definition.
    #
    # @api private
    def class_indexes
      @class_indexes ||= definition_indexes(:CLASS)
    end

    # Calculate the positions of all defined type definitions within the
    # `tokens` Array.
    #
    # Returns an Array of Hashes, each containing:
    #   :start  - An Integer position in the `tokens` Array pointing to the
    #             first Token of a defined type definition.
    #   :end    - An Integer position in the `tokens` Array pointing to the last
    #             Token of a defined type definition.
    #   :tokens - An Array consisting of all the Token objects that make up the
    #             defined type.
    #
    # @api private
    def defined_type_indexes
      @defined_type_indexes ||= definition_indexes(:DEFINE)
    end

    # Calculate the positions of all node definitions within the `tokens`
    # Array.
    #
    # Returns an Array of Hashes, each containing:
    #   :start  - An Integer position in the `tokens` Array pointing to the
    #             first Token of a defined type definition.
    #   :end    - An Integer position in the `tokens` Array pointing to the last
    #             Token of a defined type definition.
    #   :tokens - An Array consisting of all the Token objects that make up the
    #             defined type.
    #
    # @api private
    def node_indexes
      @node_indexes ||= definition_indexes(:NODE)
    end

    # Calculate the positions of all the specified defintion types within the
    # `tokens` Array.
    #
    # Returns an Array of Hashes, each containing:
    #   :start  - An Integer position in the `tokens` Array pointing to the
    #             first Token of a definition.
    #   :end    - An Integer position in the `tokens` Array pointing to the last
    #             Token of a definition.
    #   :tokens - An Array consisting of all the Token objects that make up the
    #             definition.
    #
    # @api private
    def definition_indexes(type)
      result = []
      tokens.each_with_index do |token, i|
        next unless token.type == type

        brace_depth = 0
        paren_depth = 0
        in_params = false
        inherited_class = nil
        tokens[i + 1..].each_with_index do |definition_token, j|
          case definition_token.type
          when :INHERITS
            inherited_class = definition_token.next_code_token
          when :LPAREN
            in_params = true if paren_depth.zero?
            paren_depth += 1
          when :RPAREN
            in_params = false if paren_depth == 1
            paren_depth -= 1
          when :LBRACE
            brace_depth += 1
          when :RBRACE
            brace_depth -= 1
            if brace_depth.zero? && !in_params && (token.next_code_token.type != :LBRACE)
              result << {
                start: i,
                end: i + j + 1,
                tokens: tokens[i..(i + j + 1)],
                param_tokens: param_tokens(tokens[i..(i + j + 1)]),
                type:,
                name_token: token.next_code_token,
                inherited_token: inherited_class
              }
              break
            end
          end
        end
      end
      result
    end

    # Calculate the positions of all function calls within `tokens` Array.
    #
    # Returns an Array of Hashes, each containing:
    #   :start  - An Integer position in the `tokens` Array pointing to the
    #             first Token of a function call
    #   :end    - An Integer position in the `tokens` Array pointing to the last
    #             Token of a function call
    #   :tokens - An Array consisting of all the Token objects that make up the
    #             function call.
    #
    # @api private
    def function_indexes
      @function_indexes ||= begin
        functions = []
        tokens.each_with_index do |token, token_idx|
          next unless token.type == :FUNCTION_NAME
          next unless token_idx.zero? ||
                      (token_idx == 1 && tokens[0].type == :WHITESPACE) ||
                      [:NEWLINE, :INDENT].include?(token.prev_token.type) ||
                      # function in a function
                      (token.prev_code_token && token.prev_code_token.type == :LPAREN)

          # Hash key
          next if token.next_code_token && token.next_code_token.type == :FARROW

          level = 0
          real_idx = 0
          in_paren = false
          tokens[token_idx + 1..].each_with_index do |cur_token, cur_token_idx|
            break if level.zero? && in_paren
            break if level.zero? && cur_token.type == :NEWLINE

            if cur_token.type == :LPAREN
              level += 1
              in_paren = true
            end
            level -= 1 if cur_token.type == :RPAREN
            real_idx = token_idx + 1 + cur_token_idx
          end

          functions << {
            start: token_idx,
            end: real_idx,
            tokens: tokens[token_idx..real_idx]
          }
        end
        functions
      end
    end

    # Calculate the positions of all array values within `tokens` Array.
    #
    # Returns an Array of Hashes, each containing:
    #   :start  - An Integer position in the `tokens` Array pointing to the
    #             first Token of an array value
    #   :end    - An Integer position in the `tokens` Array pointing to the last
    #             Token of an array value
    #   :tokens - An Array consisting of all the Token objects that make up the
    #             array value.
    #
    # @api private
    def array_indexes
      @array_indexes ||= begin
        arrays = []
        tokens.each_with_index do |token, token_idx|
          next unless token.type == :LBRACK

          real_idx = 0
          tokens[token_idx + 1..].each_with_index do |cur_token, cur_token_idx|
            real_idx = token_idx + 1 + cur_token_idx
            break if cur_token.type == :RBRACK
          end

          # Ignore resource references
          next if token.prev_code_token &&
                  token.prev_code_token.type == :CLASSREF

          arrays << {
            start: token_idx,
            end: real_idx,
            tokens: tokens[token_idx..real_idx]
          }
        end
        arrays
      end
    end

    # Calculate the positions of all hash values within `tokens` Array.
    #
    # Returns an Array of Hashes, each containing:
    #   :start  - An Integer position in the `tokens` Array pointing to the
    #             first Token of an hash value
    #   :end    - An Integer position in the `tokens` Array pointing to the last
    #             Token of an hash value
    #   :tokens - An Array consisting of all the Token objects that make up the
    #             hash value.
    #
    # @api private
    def hash_indexes
      @hash_indexes ||= begin
        hashes = []
        tokens.each_with_index do |token, token_idx|
          next unless token.type == :LBRACE
          next unless token.prev_code_token
          next unless [:EQUALS, :ISEQUAL, :FARROW, :LPAREN].include?(token.prev_code_token.type)

          level = 0
          real_idx = 0
          tokens[token_idx + 1..].each_with_index do |cur_token, cur_token_idx|
            real_idx = token_idx + 1 + cur_token_idx

            level += 1 if cur_token.type == :LBRACE
            level -= 1 if cur_token.type == :RBRACE
            break if level.negative?
          end

          hashes << {
            start: token_idx,
            end: real_idx,
            tokens: tokens[token_idx..real_idx]
          }
        end
        hashes
      end
    end

    # Calculate the positions of all defaults declarations within
    # `tokens` Array.
    #
    # Returns an Array of Hashes, each containing:
    #   :start  - An Integer position in the `tokens` Array pointing to the
    #             first Token of the defaults declaration
    #   :end    - An Integer position in the `tokens` Array pointing to the last
    #             Token of the defaults declaration
    #   :tokens - An Array consisting of all the Token objects that make up the
    #             defaults declaration.
    #
    # @api private
    def defaults_indexes
      @defaults_indexes ||= begin
        defaults = []
        tokens.each_with_index do |token, token_idx|
          next unless token.type == :CLASSREF
          next unless token.next_code_token && token.next_code_token.type == :LBRACE

          real_idx = 0

          tokens[token_idx + 1..].each_with_index do |cur_token, cur_token_idx|
            real_idx = token_idx + 1 + cur_token_idx
            break if cur_token.type == :RBRACE
          end

          defaults << {
            start: token_idx,
            end: real_idx,
            tokens: tokens[token_idx..real_idx]
          }
        end
        defaults
      end
    end

    # Finds all the tokens that make up the defined type or class
    # definition parameters.
    #
    # @param [Array[PuppetLint::Lexer::Token]] these_tokens
    #   An Array of PuppetLint::Lexer::Token objects that make up the defined
    #   type or class definition.
    #
    # @return [Array[PuppetLint::Lexer::Token], nil] objects or nil if it takes
    #   no parameters.
    #
    # @api private
    def param_tokens(these_tokens)
      depth = 0
      lparen_idx = nil
      rparen_idx = nil

      these_tokens.each_with_index do |token, i|
        if token.type == :LPAREN
          depth += 1
          lparen_idx = i if depth == 1
        elsif token.type == :RPAREN
          depth -= 1
          if depth.zero?
            rparen_idx = i
            break
          end
        elsif token.type == :LBRACE && depth.zero?
          # no parameters
          break
        end
      end

      if lparen_idx.nil? || rparen_idx.nil?
        nil
      else
        these_tokens[(lparen_idx + 1)..(rparen_idx - 1)]
      end
    end

    # Retrieves a list of token types that are considered to be
    # formatting tokens (whitespace, newlines, etc).
    #
    # @return [Array[Symbol]]
    #
    # @api private
    def formatting_tokens
      @formatting_tokens ||= PuppetLint::Lexer::FORMATTING_TOKENS
    end

    # Retrieves a Hash of Sets. Each key is a check name Symbol and
    # the Set of Integers returned lists all the lines that the check results
    # should be ignored on.
    #
    # @return [Hash[Symbol, Set[Integer]]]
    #
    # @api private
    def ignore_overrides
      @ignore_overrides ||= {}
    end

    # Parses all COMMENT, MLCOMMENT and SLASH_COMMENT tokens looking
    # for control comments (comments that enable or disable checks). Builds the
    # contents of the `ignore_overrides` hash.
    #
    # @return [nil]
    #
    # @api private
    def parse_control_comments
      @ignore_overrides.each_key { |check| @ignore_overrides[check].clear }

      comment_token_types = Set[:COMMENT, :MLCOMMENT, :SLASH_COMMENT]

      comment_tokens = tokens.select do |token|
        comment_token_types.include?(token.type)
      end
      control_comment_tokens = comment_tokens.select do |token|
        token.value.strip =~ %r{\Alint:(ignore:[\w\d]+|endignore)}
      end

      stack = []
      control_comment_tokens.each do |token|
        comment_data = []
        reason = []

        comment_words = token.value.strip.split(%r{\s+})
        comment_words.each_with_index do |word, i|
          if %r{\Alint:(ignore|endignore)}.match?(word)
            comment_data << word
          else
            # Once we reach the first non-controlcomment word, assume the rest
            # of the words are the reason.
            reason = comment_words[i..]
            break
          end
        end

        stack_add = []

        comment_data.each do |control|
          split_control = control.split(':')
          command = split_control[1]

          if command == 'ignore'
            check = split_control[2].to_sym
            if token.prev_token && !Set[:NEWLINE, :INDENT].include?(token.prev_token.type)
              # control comment at the end of the line, override applies to
              # a single line only
              (ignore_overrides[check] ||= {})[token.line] = reason.join(' ')
            else
              stack_add << [token.line, reason.join(' '), check]
            end
          else
            top_override = stack.pop
            if top_override.nil?
              # TODO: refactor to provide a way to expose problems from
              # PuppetLint::Data via the normal problem reporting mechanism.
              warn "WARNING: lint:endignore comment with no opening lint:ignore:<check> comment found on line #{token.line}"
            else
              top_override.each do |start|
                next if start.nil?

                (start[0]..token.line).each do |i|
                  (ignore_overrides[start[2]] ||= {})[i] = start[1]
                end
              end
            end
          end
        end
        stack << stack_add unless stack_add.empty?
      end

      stack.each do |control|
        warn "WARNING: lint:ignore:#{control[0][2]} comment on line #{control[0][0]} with no closing lint:endignore comment"
      end
    end
  end
end
