// Copyright 2012 Jonas mg
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package file

import (
	"bufio"
	"bytes"
	"io"
	"io/ioutil"
	"os"
	"regexp"
)

// editDefault represents the vaues by default to set in type edit.
type editDefault struct {
	CommentChar string // character used in comments
	//DoBackup    bool   // do backup before of edit?
}

// Values by default for type edit.
var _editDefault = editDefault{"#"}

// edit represents the file to edit.
type edit struct {
	editDefault
	file *os.File
	buf  *bufio.ReadWriter
}

type Replacer struct {
	Search, Replace string
}

type ReplacerAtLine struct {
	Line, Search, Replace string
}

// NewEdit opens a file to edit; it is created a backup.
func NewEdit(filename string) (*edit, error) {
	if err := Backup(filename); err != nil {
		return nil, err
	}

	file, err := os.OpenFile(filename, os.O_RDWR, 0666)
	if err != nil {
		return nil, err
	}

	return &edit{
		_editDefault,
		file,
		bufio.NewReadWriter(bufio.NewReader(file), bufio.NewWriter(file)),
	}, nil
}

// Append writes len(b) bytes at the end of the File. It returns an error, if any.
func (e *edit) Append(b []byte) error {
	_, err := e.file.Seek(0, io.SeekEnd)
	if err != nil {
		return err
	}

	_, err = e.file.Write(b)
	return err
}

// AppendString is like Append, but writes the contents of string s rather than
// an array of bytes.
func (e *edit) AppendString(s string) error {
	return e.Append([]byte(s))
}

// Close closes the file.
func (e *edit) Close() error {
	return e.file.Close()
}

// Comment inserts the comment character in lines that mach any regular expression
// in reLine.
func (e *edit) Comment(reLine []string) error {
	allReSearch := make([]*regexp.Regexp, len(reLine))

	for i, v := range reLine {
		if re, err := regexp.Compile(v); err != nil {
			return err
		} else {
			allReSearch[i] = re
		}
	}

	if _, err := e.file.Seek(0, io.SeekStart); err != nil {
		return err
	}

	char := []byte(e.CommentChar + " ")
	isNew := false
	buf := new(bytes.Buffer)

	// Check every line.
	for {
		line, err := e.buf.ReadBytes('\n')
		if err == io.EOF {
			break
		}

		for _, v := range allReSearch {
			if v.Match(line) {
				line = append(char, line...)

				if !isNew {
					isNew = true
				}
				break
			}
		}

		if _, err = buf.Write(line); err != nil {
			return err
		}
	}

	if isNew {
		return e.rewrite(buf.Bytes())
	}
	return nil
}

// CommentOut removes the comment character of lines that mach any regular expression
// in reLine.
func (e *edit) CommentOut(reLine []string) error {
	allSearch := make([]ReplacerAtLine, len(reLine))

	for i, v := range reLine {
		allSearch[i] = ReplacerAtLine{
			v,
			"[[:space:]]*" + e.CommentChar + "[[:space:]]*",
			"",
		}
	}

	return e.ReplaceAtLineN(allSearch, 1)
}

/*// Insert writes len(b) bytes at the start of the File. It returns an error, if any.
func (e *edit) Insert(b []byte) error {
	return e.rewrite(b)
}

// InsertString is like Insert, but writes the contents of string s rather than
// an array of bytes.
func (e *edit) InsertString(s string) error {
	return e.rewrite([]byte(s))
}*/

// Replace replaces all regular expressions mathed in r.
func (e *edit) Replace(r []Replacer) error {
	return e.genReplace(r, -1)
}

// ReplaceN replaces regular expressions mathed in r. The count determines the
// number to match:
//   n > 0: at most n matches
//   n == 0: the result is none
//   n < 0: all matches
func (e *edit) ReplaceN(r []Replacer, n int) error {
	return e.genReplace(r, n)
}

// ReplaceAtLine replaces all regular expressions mathed in r, if the line is
// matched at the first.
func (e *edit) ReplaceAtLine(r []ReplacerAtLine) error {
	return e.genReplaceAtLine(r, -1)
}

// ReplaceAtLine replaces regular expressions mathed in r, if the line is
// matched at the first. The count determines the
// number to match:
//   n > 0: at most n matches
//   n == 0: the result is none
//   n < 0: all matches
func (e *edit) ReplaceAtLineN(r []ReplacerAtLine, n int) error {
	return e.genReplaceAtLine(r, n)
}

// Generic Replace: replaces a number of regular expressions matched in r.
func (e *edit) genReplace(r []Replacer, n int) error {
	if n == 0 {
		return nil
	}
	if _, err := e.file.Seek(0, io.SeekStart); err != nil {
		return err
	}

	content, err := ioutil.ReadAll(e.buf)
	if err != nil {
		return err
	}

	isNew := false

	for _, v := range r {
		reSearch, err := regexp.Compile(v.Search)
		if err != nil {
			return err
		}

		i := n
		repl := []byte(v.Replace)

		content = reSearch.ReplaceAllFunc(content, func(s []byte) []byte {
			if !isNew {
				isNew = true
			}

			if i != 0 {
				i--
				return repl
			}
			return s
		})
	}

	if isNew {
		return e.rewrite(content)
	}
	return nil
}

// Generic ReplaceAtLine: replaces a number of regular expressions matched in r,
// if the line is matched at the first.
func (e *edit) genReplaceAtLine(r []ReplacerAtLine, n int) error {
	if n == 0 {
		return nil
	}
	if _, err := e.file.Seek(0, io.SeekStart); err != nil {
		return err
	}

	// == Cache the regular expressions
	allReLine := make([]*regexp.Regexp, len(r))
	allReSearch := make([]*regexp.Regexp, len(r))
	allRepl := make([][]byte, len(r))

	for i, v := range r {
		if reLine, err := regexp.Compile(v.Line); err != nil {
			return err
		} else {
			allReLine[i] = reLine
		}

		if reSearch, err := regexp.Compile(v.Search); err != nil {
			return err
		} else {
			allReSearch[i] = reSearch
		}

		allRepl[i] = []byte(v.Replace)
	}

	buf := new(bytes.Buffer)
	isNew := false

	// Replace every line, if it maches
	for {
		line, err := e.buf.ReadBytes('\n')
		if err == io.EOF {
			break
		}

		for i, _ := range r {
			if allReLine[i].Match(line) {
				j := n

				line = allReSearch[i].ReplaceAllFunc(line, func(s []byte) []byte {
					if !isNew {
						isNew = true
					}

					if j != 0 {
						j--
						return allRepl[i]
					}
					return s
				})
			}
		}
		if _, err = buf.Write(line); err != nil {
			return err
		}
	}

	if isNew {
		return e.rewrite(buf.Bytes())
	}
	return nil
}

func (e *edit) rewrite(b []byte) error {
	if _, err := e.file.Seek(0, io.SeekStart); err != nil {
		return err
	}

	n, err := e.file.Write(b)
	if err != nil {
		return err
	}
	if err = e.file.Truncate(int64(n)); err != nil {
		return err
	}
	return nil // e.file.Sync()
}

// * * *

// Append writes len(b) bytes at the end of the named file. It returns an
// error, if any. The file is backed up.
func Append(filename string, b []byte) error {
	e, err := NewEdit(filename)
	if err != nil {
		return err
	}

	err = e.Append(b)
	err2 := e.Close()
	if err != nil {
		return err
	}
	return err2
}

// AppendString is like Append, but writes the contents of string s rather than
// an array of bytes.
func AppendString(filename, s string) error {
	return Append(filename, []byte(s))
}

// Comment inserts the comment character in lines that mach the regular expression
// in reLine, in the named file.
func Comment(filename, reLine string) error {
	return CommentM(filename, []string{reLine})
}

// CommentM inserts the comment character in lines that mach any regular expression
// in reLine, in the named file.
func CommentM(filename string, reLine []string) error {
	e, err := NewEdit(filename)
	if err != nil {
		return err
	}

	err = e.Comment(reLine)
	err2 := e.Close()
	if err != nil {
		return err
	}
	return err2
}

// CommentOut removes the comment character of lines that mach the regular expression
// in reLine, in the named file.
func CommentOut(filename, reLine string) error {
	return CommentOutM(filename, []string{reLine})
}

// CommentOutM removes the comment character of lines that mach any regular expression
// in reLine, in the named file.
func CommentOutM(filename string, reLine []string) error {
	e, err := NewEdit(filename)
	if err != nil {
		return err
	}

	err = e.CommentOut(reLine)
	err2 := e.Close()
	if err != nil {
		return err
	}
	return err2
}

/*// Insert writes len(b) bytes at the start of the named file. It returns an
// error, if any. The file is backed up.
func Insert(filename string, b []byte) error {
	e, err := NewEdit(filename)
	if err != nil {
		return err
	}

	err = e.Insert(b)
	err2 := e.Close()
	if err != nil {
		return err
	}
	return err2
}

// InsertString is like Insert, but writes the contents of string s rather than
// an array of bytes.
func InsertString(filename, s string) error {
	return Insert(filename, []byte(s))
}*/

// Replace replaces all regular expressions mathed in r for the named file.
func Replace(filename string, r []Replacer) error {
	e, err := NewEdit(filename)
	if err != nil {
		return err
	}

	err = e.genReplace(r, -1)
	err2 := e.Close()
	if err != nil {
		return err
	}
	return err2
}

// ReplaceN replaces a number of regular expressions mathed in r for the named
// file.
func ReplaceN(filename string, r []Replacer, n int) error {
	e, err := NewEdit(filename)
	if err != nil {
		return err
	}

	err = e.genReplace(r, n)
	err2 := e.Close()
	if err != nil {
		return err
	}
	return err2
}

// ReplaceAtLine replaces all regular expressions mathed in r for the named
// file, if the line is matched at the first.
func ReplaceAtLine(filename string, r []ReplacerAtLine) error {
	e, err := NewEdit(filename)
	if err != nil {
		return err
	}

	err = e.genReplaceAtLine(r, -1)
	err2 := e.Close()
	if err != nil {
		return err
	}
	return err2
}

// ReplaceAtLineN replaces a number of regular expressions mathed in r for the
// named file, if the line is matched at the first.
func ReplaceAtLineN(filename string, r []ReplacerAtLine, n int) error {
	e, err := NewEdit(filename)
	if err != nil {
		return err
	}

	err = e.genReplaceAtLine(r, n)
	err2 := e.Close()
	if err != nil {
		return err
	}
	return err2
}
