/*
    PIP - Platform Independent Primitives
    Config parser
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/


#include "piconfig.h"

#include "pifile.h"
#include "piiostring.h"
#ifdef PIP_STD_IOSTREAM
#  include "pistring_std.h"

#  include <iostream>
#endif
/*! \class PIConfig
 * \brief Configuration file
 * \details This class provide handle access to configuration file.
 *
 * \section PIConfig_sec0 Synopsis
 * PIConfig reads configuration file and create internal dendritic
 * representation of all entries of this file. You can easily read
 * some values and write new.
 * \image html piconfig.png
 *
 * %PIConfig supports also INI-style files with sections "[section]".
 * In this case line with section name interpret as prefix to the next
 * lines. For example, these configs are equal:
 * \code
 * ser.device = /dev/ttyS0
 * ser.speed = 115200
 * debug = true
 * \endcode
 * \code
 * [ser]
 * device = /dev/ttyS0
 * speed = 115200
 * []
 * debug = true
 * \endcode
 *
 * You can use multiline values ends with " \"
 * \code
 * value = start \ #s comment
 *   _mid \
 *   _end
 * \endcode
 * In this example value = "start_mid_end"
 *
 * \section PIConfig_sec1 Concepts
 * Each node of internal tree has type PIConfig::Entry. %PIConfig
 * has one root element \a rootEntry(). Any entry of configuration file is a
 * child of this element.
 *
 */

/*! \class PIConfig::Entry
 * \brief %Entry of configuration file
 * \details This class is node of internal PIConfig tree.
 * %Entry provide access to elements of PIConfig. Each entry has
 * children or next properties:
 * * name
 * * value
 * * type
 * * comment
 *
 * Each property is a PIString. These properties forms from text line with
 * format: \code{.cpp} <name> = <value> #<type> <comment> \endcode
 * Type and comment are optional fields. Type is a single letter immediately
 * after comment symbol "#". \n \n
 * %Entry has many implicit convertions to common types: boolean, integers,
 * float, double, PIString, PIStringList. \n \n
 * Generally there is no need to create instance of %PIConfig::Entry manually,
 * it returns by functions \a getValue() of \a PIConfig, \a PIConfig::Entry or
 * \a PIConfig::Branch. If  there is no suitable entry to return, reference to
 * internal instance of %PIConfig::Entry with "default" value will be returned.
 * \snippet piconfig.cpp PIConfig::Entry
 *
 */

/*! \class PIConfig::Branch
 * \brief %Branch is a list of entries of configuration file
 * \details %Branch provides some features to get entries lists.
 * \snippet piconfig.cpp PIConfig::Branch
 *
 */


PIConfig::Entry PIConfig::Branch::_empty;
PIConfig::Entry PIConfig::Entry::_empty;


PIConfig::Branch PIConfig::Branch::allLeaves() {
	Branch b;
	b.delim = delim;
	for (auto * i: *this) {
		if (i->isLeaf())
			b << i;
		else
			allLeaves(b, i);
	}
	return b;
}


PIConfig::Entry & PIConfig::Branch::getValue(const PIString & vname, const PIString & def, bool * exist) {
	if (vname.isEmpty()) {
		_empty.clear();
		_empty.delim = delim;
		if (exist != 0) *exist = false;
		return _empty;
	}
	PIStringList tree = vname.split(delim);
	PIString name     = tree.front();
	tree.pop_front();
	Entry * ce = 0;
	for (auto * i: *this)
		if (i->_name == name) {
			ce = i;
			break;
		}
	if (ce == 0) {
		_empty._name  = vname;
		_empty._value = def;
		_empty.delim  = delim;
		if (exist != 0) *exist = false;
		return _empty;
	}
	for (auto & i: tree) {
		ce = ce->findChild(i);
		if (ce == 0) {
			_empty._name  = vname;
			_empty._value = def;
			_empty.delim  = delim;
			if (exist != 0) *exist = false;
			return _empty;
		}
	}
	if (exist != 0) *exist = true;
	return *ce;
}


PIConfig::Branch PIConfig::Branch::getValues(const PIString & name) {
	Branch b;
	b.delim = delim;
	for (auto * i: *this) {
		if (i->isLeaf()) {
			if (i->_name.find(name) >= 0) b << i;
		} else {
			for (auto * j: i->_children)
				if (j->_name.find(name) >= 0) b << j;
		}
	}
	return b;
}


PIConfig::Branch PIConfig::Branch::getLeaves() {
	Branch b;
	b.delim = delim;
	for (auto * i: *this)
		if (i->isLeaf()) b << i;
	return b;
}


PIConfig::Branch PIConfig::Branch::getBranches() {
	Branch b;
	b.delim = delim;
	for (auto * i: *this)
		if (!i->isLeaf()) b << i;
	return b;
}


PIConfig::Branch & PIConfig::Branch::filter(const PIString & f) {
	for (int i = 0; i < size_s(); ++i) {
		if (at(i)->_name.find(f) < 0) {
			remove(i);
			--i;
		}
	}
	return *this;
}


bool PIConfig::Branch::entryExists(const Entry * e, const PIString & name) const {
	if (e->_children.isEmpty()) {
		return (e->_name == name);
	}
	for (const auto * i: e->_children)
		if (entryExists(i, name)) return true;
	return false;
}


PIConfig::Entry & PIConfig::Entry::getValue(const PIString & vname, const PIString & def, bool * exist) {
	PIStringList tree = vname.split(delim);
	Entry * ce        = this;
	for (auto & i: tree) {
		ce = ce->findChild(i);
		if (ce == 0) {
			_empty._name  = vname;
			_empty._value = def;
			_empty.delim  = delim;
			if (exist != 0) *exist = false;
			return _empty;
		}
	}
	if (exist != 0) *exist = true;
	return *ce;
}


PIConfig::Branch PIConfig::Entry::getValues(const PIString & vname) {
	Branch b;
	b.delim = delim;
	for (auto * i: _children)
		if (i->_name.find(vname) >= 0) b << i;
	return b;
}


bool PIConfig::Entry::entryExists(const Entry * e, const PIString & name) const {
	if (e->_children.isEmpty()) {
		return (e->_name == name);
	}
	for (const auto * i: e->_children)
		if (entryExists(i, name)) return true;
	return false;
}


#ifdef PIP_STD_IOSTREAM
void PIConfig::Entry::coutt(std::ostream & s, const PIString & p) const {
	PIString nl = p + "  ";
	if (!_value.isEmpty())
		s << p << _name << " = " << _value << std::endl;
	else
		std::cout << p << _name << std::endl;
	for (const auto * i: _children)
		i->coutt(s, nl);
}
#endif


void PIConfig::Entry::piCoutt(PICout s, const PIString & p) const {
	PIString nl = p + "  ";
	if (!_value.isEmpty())
		s << p << _name << " = " << _value << " (" << _type << " " << _comment << ")" << PICoutManipulators::NewLine;
	else
		s << p << _name << PICoutManipulators::NewLine;
	for (const auto * i: _children)
		i->piCoutt(s, nl);
}


PIConfig::PIConfig(const PIString & path, PIIODevice::DeviceMode mode) {
	_init();
	open(path, mode);
}


PIConfig::PIConfig(PIString * string, PIIODevice::DeviceMode mode) {
	_init();
	open(string, mode);
}


PIConfig::PIConfig(PIIODevice * device, PIIODevice::DeviceMode mode) {
	_init();
	open(device, mode);
}


PIConfig::PIConfig(const PIString & path, PIStringList dirs) {
	_init();
	internal = true;
	own_dev  = true;
	dev      = new PIFile(path, PIIODevice::ReadOnly);
	incdirs  = dirs;
	incdirs << PIFile::fileInfo(path).dir();
	while (!dev->isOpened()) {
		if (dirs.isEmpty()) break;
		PIString cp = dirs.back();
		if (cp.endsWith("/") || cp.endsWith("\\")) cp.pop_back();
		cp += "/" + path;
		dev->open(cp, PIIODevice::ReadOnly);
		dirs.pop_back();
	}
	if (!dev->isOpened()) {
		delete dev;
		dev = 0;
		return;
	}
	_setupDev();
	parse();
}


PIConfig::~PIConfig() {
	root.deleteBranch();
	_destroy();
}


bool PIConfig::open(const PIString & path, PIIODevice::DeviceMode mode) {
	_destroy();
	incdirs << PIFile::fileInfo(path).dir();
	own_dev = true;
	dev     = new PIFile(path, mode);
	if (!dev->isOpened()) dev->open(path, mode);
	_setupDev();
	parse();
	return dev->isOpened();
}


bool PIConfig::open(PIString * string, PIIODevice::DeviceMode mode) {
	_destroy();
	own_dev = true;
	dev     = new PIIOString(string, PIIODevice::ReadOnly);
	_setupDev();
	parse();
	return true;
}


bool PIConfig::open(PIIODevice * device, PIIODevice::DeviceMode mode) {
	_destroy();
	own_dev = false;
	dev     = device;
	if (dev) {
		dev->open(mode);
		if (dev->isTypeOf<PIFile>()) incdirs << PIFile::fileInfo(((PIFile *)dev)->path()).dir();
	}
	_setupDev();
	parse();
	if (device) return device->isOpened();
	return false;
}


void PIConfig::_init() {
	delim         = PIStringAscii(".");
	root.delim    = delim;
	empty.delim   = delim;
	empty._parent = 0;
}


void PIConfig::_destroy() {
	piDeleteSafety(stream);
	if (own_dev && dev) delete dev;
	dev = nullptr;
	for (auto * c: inc_devs)
		delete c;
	inc_devs.clear();
}


void PIConfig::_setupDev() {
	if (!dev) return;
	stream = new PIIOTextStream(dev);
	stream->setEncoding(PIIOTextStream::UTF8);
}


void PIConfig::_clearDev() {
	if (!dev) return;
	if (PIString(dev->className()) == "PIFile") {
		((PIFile *)dev)->clear();
		return;
	}
	if (PIString(dev->className()) == "PIIOString") {
		((PIIOString *)dev)->clear();
		((PIIOString *)dev)->setMode(PIIODevice::WriteOnly);
		return;
	}
}


void PIConfig::_flushDev() {
	if (!dev) return;
	if (PIString(dev->className()) == "PIFile") {
		((PIFile *)dev)->flush();
	}
}


bool PIConfig::_isEndDev() {
	if (!stream) return true;
	return stream->isEnd();
}


void PIConfig::_seekToBeginDev() {
	if (!dev) return;
	if (PIString(dev->className()) == "PIFile") {
		((PIFile *)dev)->seekToBegin();
		return;
	}
	if (PIString(dev->className()) == "PIIOString") {
		((PIIOString *)dev)->seekToBegin();
		((PIIOString *)dev)->setMode(PIIODevice::ReadOnly);
		return;
	}
}


PIString PIConfig::_readLineDev() {
	if (!stream) return PIString();
	return stream->readLine();
}


void PIConfig::_writeDev(const PIString & l) {
	// piCout << "write \"" << l << "\"";
	if (!stream) return;
	stream->append(l);
}


bool PIConfig::isOpened() const {
	if (dev) return dev->isOpened();
	return false;
}


PIConfig::Entry & PIConfig::getValue(const PIString & vname, const PIString & def, bool * exist) {
	PIStringList tree = vname.split(delim);
	Entry * ce        = &root;
	for (auto & i: tree) {
		ce = ce->findChild(i);
		if (ce == 0) {
			if (exist != 0) *exist = false;
			empty._name  = vname;
			empty._value = def;
			empty.delim  = delim;
			return empty;
		}
	}
	if (exist != 0) *exist = true;
	return *ce;
}


PIConfig::Branch PIConfig::getValues(const PIString & vname) {
	Branch b;
	b.delim = delim;
	for (auto * i: root._children)
		if (i->_name.find(vname) >= 0) b << i;
	return b;
};


void PIConfig::addEntry(const PIString & name, const PIString & value, const PIString & type, bool write) {
	if (getValue(name)._parent != 0) return;
	bool toRoot       = false;
	PIStringList tree = name.split(delim);
	PIString ename    = tree.back();
	tree.pop_back();
	Entry *te, *ce, *entry = &root;
	if (tree.isEmpty()) toRoot = true;
	for (auto & i: tree) {
		te = entry->findChild(i);
		if (te == 0) {
			ce          = new Entry();
			ce->delim   = delim;
			ce->_tab    = entry->_tab;
			ce->_line   = entry->_line;
			ce->_name   = i;
			ce->_parent = entry;
			entry->_children << ce;
			entry = ce;
		} else
			entry = te;
	}
	PIConfig::Branch ch = entry->_children;
	ch.sort(PIConfig::Entry::compare);
	te         = (entry->isLeaf() ? 0 : ch.back());
	ce         = new Entry();
	ce->delim  = delim;
	ce->_name  = ename;
	ce->_value = value;
	ce->_type  = type;
	if (te == 0) {
		ce->_tab = entry->_tab;
		if (toRoot)
			ce->_line = other.size_s() - 1;
		else
			ce->_line = entry->_line;
	} else {
		ce->_tab = te->_tab;
		if (toRoot)
			ce->_line = other.size_s() - 1;
		else {
			ch = entry->_parent->_children;
			ch.sort(PIConfig::Entry::compare);
			ce->_line = ch.back()->_line + 1;
		}
	}
	ce->_parent = entry;
	entry->_children << ce;
	other.insert(ce->_line, "");
	Branch b   = allLeaves();
	bool found = false;
	for (int i = 0; i < b.size_s(); ++i) {
		if (found) {
			b[i]->_line++;
			continue;
		}
		if (b[i] == ce) {
			found = true;
			if (i > 0)
				if (b[i - 1]->_line == b[i]->_line) b[i - 1]->_line++;
		}
	}
	if (write) writeAll();
}


void PIConfig::setValue(const PIString & name, const PIString & value, const PIString & type, bool write) {
	Entry & e(getValue(name));
	if (&e == &empty) {
		addEntry(name, value, type, write);
		return;
	}
	e._value = value;
	e._type  = type;
	if (write) writeAll();
}


int PIConfig::entryIndex(const PIString & name) {
	PIStringList tree = name.split(delim);
	Entry * ce        = &root;
	for (auto & i: tree) {
		ce = ce->findChild(i);
		if (ce == 0) return -1;
	}
	return allLeaves().indexOf(ce);
}


void PIConfig::setValue(uint number, const PIString & value, bool write) {
	Entry & e(entryByIndex(number));
	if (&e == &empty) return;
	e._value = value;
	if (write) writeAll();
}


void PIConfig::setName(uint number, const PIString & name, bool write) {
	Entry & e(entryByIndex(number));
	if (&e == &empty) return;
	e._name = name;
	if (write) writeAll();
}


void PIConfig::setType(uint number, const PIString & type, bool write) {
	Entry & e(entryByIndex(number));
	if (&e == &empty) return;
	e._type = type;
	if (write) writeAll();
}


void PIConfig::setComment(uint number, const PIString & comment, bool write) {
	Entry & e(entryByIndex(number));
	if (&e == &empty) return;
	e._comment = comment;
	if (write) writeAll();
}


void PIConfig::removeEntry(const PIString & name, bool write) {
	Entry & e(getValue(name));
	if (&e == &empty) return;
	Branch b = allLeaves();
	removeEntry(b, &e);
	if (write) writeAll();
}


void PIConfig::removeEntry(uint number, bool write) {
	Entry & e(entryByIndex(number));
	if (&e == &empty) return;
	Branch b = allLeaves();
	removeEntry(b, &e);
	if (write) writeAll();
}


void PIConfig::removeEntry(Branch & b, PIConfig::Entry * e) {
	bool leaf = true;
	if (e->isLeaf()) other.remove(e->_line);
	if (!e->isLeaf() && !e->_value.isEmpty()) {
		e->_value.clear();
		leaf = false;
	} else {
		int cc = e->_children.size_s();
		piForTimes(cc) removeEntry(b, e->_children.back());
	}
	bool found = false;
	for (int i = 0; i < b.size_s(); ++i) {
		if (found) {
			b[i]->_line--;
			continue;
		}
		if (b[i] == e) found = true;
	}
	if (!leaf) return;
	e->_parent->_children.removeOne(e);
	b.removeOne(e);
	delete e;
}


PIString PIConfig::getPrefixFromLine(PIString line, bool * exists) {
	line.trim();
	if (line.left(1) == "#") {
		if (exists) *exists = false;
		return PIString();
	}
	int ci = line.find("#");
	if (ci >= 0) line.cutRight(line.size() - ci);
	if (line.find("=") >= 0) {
		if (exists) *exists = false;
		return PIString();
	}
	if (line.find("[") >= 0 && line.find("]") >= 0) {
		if (exists) *exists = true;
		return line.takeRange('[', ']').trim();
	}
	if (exists) *exists = false;
	return PIString();
}


void PIConfig::writeAll() {
	// cout << this << " write < " << size() << endl;
	_clearDev();
	buildFullNames(&root);
	Branch b = allLeaves();
	PIString prefix, tprefix;
	bool isPrefix;
	// for (int i = 0; i < b.size_s(); ++i)
	//	cout << b[i]->_name << " = " << b[i]->_value << endl;
	int j = 0;
	for (int i = 0; i < other.size_s(); ++i) {
		// cout << j << endl;
		if (j >= 0 && j < b.size_s()) {
			if (b[j]->_line == i) {
				b[j]->buildLine();
				_writeDev((b[j]->_all).cutLeft(prefix.size()) + "\n");
				// cout << this << " " << b[j]->_all << endl;
				++j;
			} else {
				_writeDev(other[i]);
				tprefix = getPrefixFromLine(other[i], &isPrefix);
				if (isPrefix) {
					prefix = tprefix;
					if (!prefix.isEmpty()) prefix += delim;
				}
				if (i < other.size_s() - 1) _writeDev('\n');
				// cout << this << " " << other[i] << endl;
			}
		} else {
			_writeDev(other[i]);
			tprefix = getPrefixFromLine(other[i], &isPrefix);
			if (isPrefix) {
				prefix = tprefix;
				if (!prefix.isEmpty()) prefix += delim;
			}
			if (i < other.size_s() - 1) _writeDev('\n');
			// cout << this << " " << other[i] << endl;
		}
	}
	_flushDev();
	readAll();
	// cout << this << " write > " << size() << endl;
}


void PIConfig::clear() {
	_clearDev();
	parse();
}


void PIConfig::readAll() {
	root.deleteBranch();
	root.clear();
	parse();
}


bool PIConfig::entryExists(const Entry * e, const PIString & name) const {
	if (e->_children.isEmpty()) {
		return (e->_name == name);
	}
	for (const auto * i: e->_children)
		if (entryExists(i, name)) return true;
	return false;
}


void PIConfig::updateIncludes() {
	if (internal) return;
	all_includes.clear();
	for (auto * c: includes)
		all_includes << c->allLeaves();
}


PIString PIConfig::parseLine(PIString v) {
	int i = -1, l = 0;
	while (1) {
		i = v.find("${");
		if (i < 0) break;
		PIString w = v.mid(i + 1).takeRange('{', '}'), r;
		l          = w.length() + 3;
		w          = parseLine(w);
		w.trim();
		bool ex              = false;
		PIConfig::Entry & me = getValue(w, "", &ex);
		if (ex) {
			r = me._value;
		} else {
			for (const auto * e: all_includes)
				if (e->_full_name == w) {
					r = e->_value;
					break;
				}
		}
		v.replace(i, l, r);
	}
	return v;
}


void PIConfig::parse() {
	// piCout << "[PIConfig] charset" << PIFile::defaultCharset();
	PIString src, str, tab, comm, all, name, type, prefix, tprefix;
	PIStringList tree;
	Entry *entry = 0, *te = 0, *ce = 0;
	int ind, sind;
	bool isNew = false, isPrefix = false, wasMultiline = false, isMultiline = false;
	piDeleteAllAndClear(inc_devs);
	includes.clear();
	if (!isOpened()) return;
	_seekToBeginDev();
	other.clear();
	lines = 0;
	while (!_isEndDev()) {
		other.push_back(PIString());
		src = str = parseLine(_readLineDev());
		tprefix   = getPrefixFromLine(src, &isPrefix);
		if (isPrefix) {
			prefix = tprefix;
			if (!prefix.isEmpty()) prefix += delim;
		}
		// piCout << "line \"" << str << "\"";
		tab = str.left(str.find(str.trimmed().left(1)));
		str.trim();
		all  = str;

		sind = str.find('#');
		if (sind > 0) {
			comm = str.mid(sind + 1).trimmed();
			if (!comm.isEmpty()) {
				type = comm[0];
				comm.cutLeft(1).trim();
			} else
				type = "s";
			str = str.left(sind).trim();
		} else {
			type = "s";
			comm = "";
		}

		if (str.endsWith(" \\")) {
			isMultiline = true;
			str.cutRight(2).trim();
		} else
			isMultiline = false;

		if (wasMultiline) {
			wasMultiline = false;
			if (ce) {
				ce->_value += str;
				ce->_all += " \\\n" + all;
			}
			str.clear();
		} else
			ce = 0;
		wasMultiline = isMultiline;

		// piCout << "[PIConfig] str" << str.size() << str << str.toUTF8();
		ind          = str.find('=');
		if ((ind > 0) && (str[0] != '#')) {
			tree = (prefix + str.left(ind).trimmed()).split(delim);
			if (tree.front() == "include") {
				name             = str.mid(ind + 1).trimmed();
				PIConfig * iconf = new PIConfig(name, incdirs);
				// piCout << "include" << name << iconf->dev;
				if (!iconf->dev) {
					delete iconf;
				} else {
					inc_devs << iconf;
					includes << iconf << iconf->includes;
					updateIncludes();
				}
				other.back() = src;
			} else {
				name = tree.back();
				tree.pop_back();
				entry = &root;
				for (const auto & i: tree) {
					te = entry->findChild(i);
					if (te == 0) {
						ce          = new Entry();
						ce->delim   = delim;
						ce->_tab    = tab;
						ce->_line   = lines;
						ce->_name   = i;
						ce->_parent = entry;
						entry->_children << ce;
						entry = ce;
					} else
						entry = te;
				}
				isNew = false;
				ce    = entry->findChild(name);
				if (ce == 0) {
					ce    = new Entry();
					isNew = true;
				}
				ce->delim    = delim;
				ce->_tab     = tab;
				ce->_name    = name;
				ce->_value   = str.mid(ind + 1).trimmed();
				ce->_type    = type;
				ce->_comment = comm;
				// piCout << "[PIConfig] comm" << comm.size() << comm << comm.toUTF8();
				// piCout << "[PIConfig] type" << type.size() << type << type.toUTF8();
				ce->_line    = lines;
				ce->_all     = all;
				if (isNew) {
					ce->_parent = entry;
					entry->_children << ce;
				}
			}
		} else
			other.back() = src;
		lines++;
	}
	setEntryDelim(&root, delim);
	buildFullNames(&root);
}


#ifdef PIP_STD_IOSTREAM
std::ostream & operator<<(std::ostream & s, const PIConfig::Entry & v) {
	s << v.value();
	return s;
}


std::ostream & operator<<(std::ostream & s, const PIConfig::Branch & v) {
	v.coutt(s, "");
	return s;
}
#endif
