package gcfg

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"

	"github.com/go-git/gcfg/scanner"
	"github.com/go-git/gcfg/token"
	"gopkg.in/warnings.v0"
)

var unescape = map[rune]rune{'\\': '\\', '"': '"', 'n': '\n', 't': '\t', 'b': '\b'}

// no error: invalid literals should be caught by scanner
func unquote(s string) string {
	u, q, esc := make([]rune, 0, len(s)), false, false
	for _, c := range s {
		if esc {
			uc, ok := unescape[c]
			switch {
			case ok:
				u = append(u, uc)
				fallthrough
			case !q && c == '\n':
				esc = false
				continue
			}
			panic("invalid escape sequence")
		}
		switch c {
		case '"':
			q = !q
		case '\\':
			esc = true
		default:
			u = append(u, c)
		}
	}
	if q {
		panic("missing end quote")
	}
	if esc {
		panic("invalid escape sequence")
	}
	return string(u)
}

func read(c *warnings.Collector, callback func(string, string, string, string, bool) error,
	fset *token.FileSet, file *token.File, src []byte) error {
	//
	var s scanner.Scanner
	var errs scanner.ErrorList
	s.Init(file, src, func(p token.Position, m string) { errs.Add(p, m) }, 0)
	sect, sectsub := "", ""
	pos, tok, lit := s.Scan()
	errfn := func(msg string) error {
		return fmt.Errorf("%s: %s", fset.Position(pos), msg)
	}
	for {
		if errs.Len() > 0 {
			if err := c.Collect(errs.Err()); err != nil {
				return err
			}
		}
		switch tok {
		case token.EOF:
			return nil
		case token.EOL, token.COMMENT:
			pos, tok, lit = s.Scan()
		case token.LBRACK:
			pos, tok, lit = s.Scan()
			if errs.Len() > 0 {
				if err := c.Collect(errs.Err()); err != nil {
					return err
				}
			}
			if tok != token.IDENT {
				if err := c.Collect(errfn("expected section name")); err != nil {
					return err
				}
			}
			sect, sectsub = lit, ""
			pos, tok, lit = s.Scan()
			if errs.Len() > 0 {
				if err := c.Collect(errs.Err()); err != nil {
					return err
				}
			}
			if tok == token.STRING {
				sectsub = unquote(lit)
				if sectsub == "" {
					if err := c.Collect(errfn("empty subsection name")); err != nil {
						return err
					}
				}
				pos, tok, lit = s.Scan()
				if errs.Len() > 0 {
					if err := c.Collect(errs.Err()); err != nil {
						return err
					}
				}
			}
			if tok != token.RBRACK {
				if sectsub == "" {
					if err := c.Collect(errfn("expected subsection name or right bracket")); err != nil {
						return err
					}
				}
				if err := c.Collect(errfn("expected right bracket")); err != nil {
					return err
				}
			}
			pos, tok, lit = s.Scan()
			if tok != token.EOL && tok != token.EOF && tok != token.COMMENT {
				if err := c.Collect(errfn("expected EOL, EOF, or comment")); err != nil {
					return err
				}
			}
			// If a section/subsection header was found, ensure a
			// container object is created, even if there are no
			// variables further down.
			err := c.Collect(callback(sect, sectsub, "", "", true))
			if err != nil {
				return err
			}
		case token.IDENT:
			if sect == "" {
				if err := c.Collect(errfn("expected section header")); err != nil {
					return err
				}
			}
			n := lit
			pos, tok, lit = s.Scan()
			if errs.Len() > 0 {
				return errs.Err()
			}
			blank, v := tok == token.EOF || tok == token.EOL || tok == token.COMMENT, ""
			if !blank {
				if tok != token.ASSIGN {
					if err := c.Collect(errfn("expected '='")); err != nil {
						return err
					}
				}
				pos, tok, lit = s.Scan()
				if errs.Len() > 0 {
					if err := c.Collect(errs.Err()); err != nil {
						return err
					}
				}
				if tok != token.STRING {
					if err := c.Collect(errfn("expected value")); err != nil {
						return err
					}
				}
				v = unquote(lit)
				pos, tok, lit = s.Scan()
				if errs.Len() > 0 {
					if err := c.Collect(errs.Err()); err != nil {
						return err
					}
				}
				if tok != token.EOL && tok != token.EOF && tok != token.COMMENT {
					if err := c.Collect(errfn("expected EOL, EOF, or comment")); err != nil {
						return err
					}
				}
			}
			err := c.Collect(callback(sect, sectsub, n, v, blank))
			if err != nil {
				return err
			}
		default:
			if sect == "" {
				if err := c.Collect(errfn("expected section header")); err != nil {
					return err
				}
			}
			if err := c.Collect(errfn("expected section header or variable declaration")); err != nil {
				return err
			}
		}
	}
	panic("never reached")
}

func readInto(config interface{}, fset *token.FileSet, file *token.File,
	src []byte) error {
	//
	c := warnings.NewCollector(isFatal)
	firstPassCallback := func(s string, ss string, k string, v string, bv bool) error {
		return set(c, config, s, ss, k, v, bv, false)
	}
	err := read(c, firstPassCallback, fset, file, src)
	if err != nil {
		return err
	}
	secondPassCallback := func(s string, ss string, k string, v string, bv bool) error {
		return set(c, config, s, ss, k, v, bv, true)
	}
	err = read(c, secondPassCallback, fset, file, src)
	if err != nil {
		return err
	}
	return c.Done()
}

// ReadWithCallback reads gcfg formatted data from reader and calls
// callback with each section and option found.
//
// Callback is called with section, subsection, option key, option value
// and blank value flag as arguments.
//
// When a section is found, callback is called with nil subsection, option key
// and option value.
//
// When a subsection is found, callback is called with nil option key and
// option value.
//
// If blank value flag is true, it means that the value was not set for an option
// (as opposed to set to empty string).
//
// If callback returns an error, ReadWithCallback terminates with an error too.
func ReadWithCallback(reader io.Reader, callback func(string, string, string, string, bool) error) error {
	src, err := ioutil.ReadAll(reader)
	if err != nil {
		return err
	}

	fset := token.NewFileSet()
	file := fset.AddFile("", fset.Base(), len(src))
	c := warnings.NewCollector(isFatal)

	return read(c, callback, fset, file, src)
}

// ReadInto reads gcfg formatted data from reader and sets the values into the
// corresponding fields in config.
func ReadInto(config interface{}, reader io.Reader) error {
	src, err := ioutil.ReadAll(reader)
	if err != nil {
		return err
	}
	fset := token.NewFileSet()
	file := fset.AddFile("", fset.Base(), len(src))
	return readInto(config, fset, file, src)
}

// ReadStringInto reads gcfg formatted data from str and sets the values into
// the corresponding fields in config.
func ReadStringInto(config interface{}, str string) error {
	r := strings.NewReader(str)
	return ReadInto(config, r)
}

// ReadFileInto reads gcfg formatted data from the file filename and sets the
// values into the corresponding fields in config.
func ReadFileInto(config interface{}, filename string) error {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close()
	src, err := ioutil.ReadAll(f)
	if err != nil {
		return err
	}
	fset := token.NewFileSet()
	file := fset.AddFile(filename, fset.Base(), len(src))
	return readInto(config, fset, file, src)
}
