require "compiler/crystal/syntax"
require "../../lsp/protocol"

module Lattice
  module Psi
    # PsiFile wraps a Crystal AST node tree together with the originating URI
    # and source text.  It is the entry point for all PSI (Program Structure
    # Interface) operations on a single source file.
    #
    # Usage:
    #
    # ```crystal
    # psi = PsiFile.new("file:///path/to/foo.cr", source)
    # psi.nodes.each { |node| ... }
    # ```
    class PsiFile
      getter uri : String
      getter source : String
      getter ast : ::Crystal::ASTNode?

      def initialize(@uri : String, @source : String)
        @ast = parse_source
      end

      # Convenience factory used by specs and tooling.
      # Accepts a plain file path (not a URI), the source text, and an optional
      # `WorkspaceIndex` (reserved for future use — ignored for now).
      def self.from_source(path : String, source : String, index = nil) : PsiFile
        uri = if path.starts_with?("file://")
          path
        elsif path =~ /^([a-zA-Z]):\\/
          drive = path[0].to_s
          rest  = path[2..].gsub('\\', '/')
          "file:///#{drive}%3A#{rest}"
        else
          "file:///#{path.gsub('\\', '/')}"
        end
        new(uri, source)
      end

      # Re-parse after the source has changed (e.g. after an incremental sync).
      def reparse(new_source : String)
        @source = new_source
        @ast = parse_source
      end

      # Returns the file-system path derived from the URI.
      def path : String
        return @uri unless @uri.starts_with?("file://")
        p = @uri.sub(/^file:\/\//, "")
        p = URI.decode(p)
        p = p[1..] if p =~ /^\/([a-zA-Z]):/
        p.gsub('/', File::SEPARATOR)
      end

      # Yields every top-level AST node in the file.
      def each_node(&block : ::Crystal::ASTNode ->)
        root = @ast
        return unless root.is_a?(::Crystal::Expressions)
        root.expressions.each { |n| block.call(n) }
      end

      # Walk the entire tree, yielding every node.
      def walk(&block : ::Crystal::ASTNode ->)
        return unless ast = @ast
        Walker.new(block).accept(ast)
      end

      private def parse_source : ::Crystal::ASTNode?
        parser = ::Crystal::Parser.new(@source)
        parser.filename = path
        parser.parse
      rescue ex : ::Crystal::SyntaxException
        nil
      end

      # Simple recursive visitor used by `#walk`.
      private class Walker < ::Crystal::Visitor
        def initialize(@block : ::Crystal::ASTNode ->)
        end

        def visit(node : ::Crystal::ASTNode) : Bool
          @block.call(node)
          true
        end

        def visit(node : ::Crystal::Nop) : Bool
          false
        end
      end
    end
  end
end
