/*
Package analyzer implements a semantic analyzer for the ARF language. In it,
there is a function called Analyze which takes in a module path and returns a
table of sections representative of that module. The result of this operation is
not cached.

The section table returned by Analyze can be expected to be both syntactically
correct and semantically sound.

This package automatically invokes the parser and lexer packages.
*/
package analyzer

import "os"
import "fmt"
import "path/filepath"
import "git.tebibyte.media/arf/arf/types"
import "git.tebibyte.media/arf/arf/parser"
import "git.tebibyte.media/arf/arf/infoerr"

// analysisOperation holds information about an ongoing analysis operation.
type analysisOperation struct {
	sectionTable SectionTable
	modulePath   string

	currentPosition locator
	currentSection  parser.Section
	currentTree     parser.SyntaxTree
}

// Analyze performs a semantic analysis on the module specified by path, and
// returns a SectionTable that can be translated into C. The result of this is
// not cached.
func Analyze (modulePath string, skim bool) (table SectionTable, err error) {
	if modulePath[0] != '/' {
		cwd, _ := os.Getwd()
		modulePath = filepath.Join(cwd, modulePath)
	}

	analyzer := analysisOperation {
		sectionTable: make(SectionTable),
		modulePath:   modulePath,
	}

	err = analyzer.analyze()
	table = analyzer.sectionTable
	return
}

// analyze performs an analysis operation given the state of the operation
// struct.
func (analyzer *analysisOperation) analyze () (err error) {
	var tree parser.SyntaxTree
	tree, err = parser.Fetch(analyzer.modulePath, false)
	if err != nil { return }
	sections := tree.Sections()

	for !sections.End() {
		_, err = analyzer.fetchSection(locator {
			modulePath: analyzer.modulePath,
			name: sections.Key(),
		})
		if err != nil { return err }
		sections.Next()
	}
	
	return
}

// fetchSection returns a section from the section table. If it has not already
// been analyzed, it analyzes it first. If the section does not actually exist,
// a nil section is returned. When this happens, an error should be created on
// whatever syntax tree node "requested" the section be analyzed.
func (analyzer *analysisOperation) fetchSection (
	where locator,
) (
	section Section,
	err error,
) {
	var exists bool
	section, exists = analyzer.resolvePrimitive(where)
	if exists { return }

	section, exists = analyzer.sectionTable[where]
	if exists { return }

	// fetch the module. since we already have our main module parsed fully
	// and not skimmed, we can just say "yeah lets skim stuff here".
	var tree parser.SyntaxTree
	tree, err = parser.Fetch(where.modulePath, true)
	if err != nil {
		section = nil
		return
	}

	var parsedSection = tree.LookupSection("", where.name)
	if parsedSection == nil {
		section = nil
		return
	}

	previousPosition := analyzer.currentPosition
	previousSection  := analyzer.currentSection
	previousTree     := analyzer.currentTree
	analyzer.currentPosition = where
	analyzer.currentSection  = parsedSection
	analyzer.currentTree     = tree

	defer func () {
		analyzer.currentPosition = previousPosition
		analyzer.currentSection  = previousSection
		analyzer.currentTree     = previousTree
	} ()

	// analyze section. have analysis methods work on currentPosition
	// and currentSection.
	// 
	// while building an analyzed section, add it to the section
	// table as soon as the vital details are acquired, and mark it as
	// incomplete. that way, it can still be referenced by itself in certain
	// scenarios.
	switch parsedSection.(type) {
	case parser.TypeSection:
		section, err = analyzer.analyzeTypeSection()
		if err != nil { return}
	case parser.EnumSection:
		section, err = analyzer.analyzeEnumSection()
		if err != nil { return}
	case parser.FaceSection:
	case parser.DataSection:
		section, err = analyzer.analyzeDataSection()
		if err != nil { return}
	case parser.FuncSection:
		section, err = analyzer.analyzeFuncSection()
		if err != nil { return}
	}
	
	return
}

// resolvePrimitive checks to see if the locator is in the current module, and
// refers to a primitive. If it does, it returns a pointer to that primitive
// and true for exists. If it doesn't, it returns nil and false. this method is
// only to be used by analysisOperation.fetchSection.
func (analyzer *analysisOperation) resolvePrimitive (
	where locator,
) (
	section Section,
	exists bool,
) {
	// primitives are scoped as if they are contained within the current
	// module, so if the location refers to something outside of the current
	// module, it is definetly not referring to a primitive.
	if where.modulePath != analyzer.currentPosition.modulePath {
		return
	}

	exists = true
	switch where.name {
	case "Int":    section = &PrimitiveInt
	case "UInt":   section = &PrimitiveUInt
	case "I8":     section = &PrimitiveI8
	case "I16":    section = &PrimitiveI16
	case "I32":    section = &PrimitiveI32
	case "I64":    section = &PrimitiveI64
	case "U8":     section = &PrimitiveU8
	case "U16":    section = &PrimitiveU16
	case "U32":    section = &PrimitiveU32
	case "U64":    section = &PrimitiveU64
	case "Obj":    section = &PrimitiveObj
	// case "Face":   section = &PrimitiveFace
	// case "Func":   section = &PrimitiveFunc
	case "String": section = &BuiltInString
	default:
		exists = false
	}

	return
}

// TODO: make this method a generalized "get this from an identifier in context
// of the current scope" method. have it return various things like sections,
// variables, functions, members, methods, etc. have it return an any.
// if no node could be found, return an error saying entity not found. if the
// node is private, return an error.
//
// when new things are defined, they should not be allowed to shadow anything
// else in above scopes. nevertheless, the method should search in this order:
// 
// 1. search scopes starting with closest -> farthest
// 2. if first part of identifier is a require, get section from other module
// 3. search for section in current module

// fetchNodeFromIdentifier is like fetchSection, but takes in an identifier
// referring to any node accessible within the current scope and returns it.
// This method works within the current scope and current module. This method
// consumes items from the input identifier, and outputs the items which it did
// not consume.
func (analyzer *analysisOperation) fetchNodeFromIdentifier (
	which parser.Identifier,
) (
	node any,
	bitten parser.Identifier,
	err error,
) {
	var item string
	item, bitten = which.Bite()

	// TODO: search scopes for variables

	// the identifier must be referring to a section
	var external bool
	path, exists := analyzer.currentTree.ResolveRequire(item)
	if exists {
		// we have our module path, so get the section name
		item, bitten = bitten.Bite()
		external = true
	} else {
		// that wasn't a module name, so the module path must be our
		// current one
		path = analyzer.currentPosition.modulePath
	}

	// attempt to get section
	var section Section
	section, err = analyzer.fetchSection (locator {
		name:       item,
		modulePath: path,
	})
	node = section
	if err != nil { return }

	// return error if nothing mentioned in the identifier is accessible
	if node == nil {
		err = which.NewError (
			"can't find anything called \"" + item + "\" within " +
			"current scope",
			infoerr.ErrorKindError,
		)
		return
	}

	// return error if the section is private
	if external && section.Permission() == types.PermissionPrivate {
		err = which.NewError(
			"this section is private, and cannot be used " +
			"outside of its module",
			infoerr.ErrorKindError)
		return
	}
	
	return
}

// addSection adds a section to the analyzer's section table. If a section with
// that name already exists, it panics because the parser should not have given
// that to us.
func (analyzer *analysisOperation) addSection (section Section) {
	_, exists := analyzer.sectionTable[section.locator()]
	if exists {
		panic (
			"invalid state: duplicate section " +
			section.locator().ToString())
	}
	analyzer.sectionTable[section.locator()] = section
	return
}

// typeCheck checks to see if source can fit as an argument into a slot of type
// destination. If it can, it retunrs nil. If it can't, it returns an error
// explaining why.
func (analyzer *analysisOperation) typeCheck (
	source Argument,
	destination Type,
) (
	err error,
) {
	if !source.canBePassedAs(destination) {
		err = source.NewError (
			typeMismatchErrorMessage (
				source.What(),
				destination),
			infoerr.ErrorKindError)
	}

	return
}

// inCurrentModule returns whether or not the specified section resides within
// the current module.
func (analyzer *analysisOperation) inCurrentModule (
	section Section,
) (
	inCurrent bool,
){
	inCurrent =
		section.locator().modulePath ==
		analyzer.currentPosition.modulePath
	return
}

// TODO: make a method of analyzer that, given a name, searches through all
// accessible scopes and returns the thing the name references. when analyzing
// a function, the analyzer should remember a trail of scopes.

// doIndent perfroms a fmt.Sprint operation on input, indenting the string. This
// does not add a trailing newline.
func doIndent (indent int, input ...any) (output string) {
	for index := 0; index < indent; index ++ {
		output += "\t"
	}

	output += fmt.Sprint(input...)
	return
}
