/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <log4cxx/logstring.h>
#include <log4cxx/helpers/properties.h>
#include <log4cxx/helpers/inputstreamreader.h>
#include <log4cxx/helpers/exception.h>
#include <log4cxx/helpers/pool.h>
#include <cctype>
#include <algorithm>

using namespace LOG4CXX_NS;
using namespace LOG4CXX_NS::helpers;

template <typename Char>
struct Properties::Less
{
	bool operator()(const std::basic_string<Char>& str1, const std::basic_string<Char>& str2) const
	{
		return std::lexicographical_compare
			( str1.begin(), str1.end()
			, str2.begin(), str2.end()
			, [](Char c1, Char c2)
				{
					return std::tolower(static_cast<unsigned char>(c1)) < std::tolower(static_cast<unsigned char>(c2));
				}
			);
	}
};

class PropertyParser
{
	public:
		void parse(LogString& in, Properties& properties)
		{
			LogString key, element;
			LexemType lexemType = BEGIN;
			logchar c;
			bool finished = false;

			if (!get(in, c))
			{
				return;
			}

			while (!finished)
			{
				switch (lexemType)
				{
					case BEGIN:
						switch (c)
						{
							case 0x20: // ' '
							case 0x09: // '\t'
							case 0x0A: // '\n'
							case 0x0D: // '\r'
								if (!get(in, c))
								{
									finished = true;
								}

								break;

							case 0x23: // '#'
							case 0x21: // '!'
								lexemType = COMMENT;

								if (!get(in, c))
								{
									finished = true;
								}

								break;

							default:
								lexemType = KEY;
								break;
						}

						break;

					case KEY:
						switch (c)
						{
							case 0x5C: // '\\'
								lexemType = KEY_ESCAPE;

								if (!get(in, c))
								{
									finished = true;
								}

								break;

							case 0x09: // '\t'
							case 0x20: // ' '
							case 0x3A: // ':'
							case 0x3D: // '='
								lexemType = DELIMITER;

								if (!get(in, c))
								{
									finished = true;
								}

								break;

							case 0x0A:
							case 0x0D:
								// key associated with an empty string element
								properties.setProperty(key, LogString());
								key.erase(key.begin(), key.end());
								lexemType = BEGIN;

								if (!get(in, c))
								{
									finished = true;
								}

								break;

							default:
								key.append(1, c);

								if (!get(in, c))
								{
									finished = true;
								}

								break;
						}

						break;

					case KEY_ESCAPE:
						switch (c)
						{
							case 0x74: // 't'
								key.append(1, 0x09);
								lexemType = KEY;
								break;

							case 0x6E: // 'n'
								key.append(1, 0x0A);
								lexemType = KEY;
								break;

							case 0x72: // 'r'
								key.append(1, 0x0D);
								lexemType = KEY;
								break;

							case 0x0A: // '\n'
								lexemType = KEY_CONTINUE;
								break;

							case 0x0D: // '\r'
								lexemType = KEY_CONTINUE2;
								break;

							default:
								key.append(1, c);
								lexemType = KEY;
						}

						if (!get(in, c))
						{
							finished = true;
						}

						break;

					case KEY_CONTINUE:
						switch (c)
						{
							case 0x20:  // ' '
							case 0x09: //  '\t'
								if (!get(in, c))
								{
									finished = true;
								}

								break;

							default:
								lexemType = KEY;
								break;
						}

						break;

					case KEY_CONTINUE2:
						switch (c)
						{
							case 0x0A: // '\n'
								if (!get(in, c))
								{
									finished = true;
								}

								lexemType = KEY_CONTINUE;
								break;

							default:
								lexemType = KEY_CONTINUE;
								break;
						}

						break;

					case DELIMITER:
						switch (c)
						{
							case 0x09: // '\t'
							case 0x20: // ' '
							case 0x3A: // ':'
							case 0x3D: // '='
								if (!get(in, c))
								{
									finished = true;
								}

								break;

							default:
								lexemType = ELEMENT;
								break;
						}

						break;

					case ELEMENT:
						switch (c)
						{
							case 0x5C: // '\\'
								lexemType = ELEMENT_ESCAPE;

								if (!get(in, c))
								{
									finished = true;
								}

								break;

							case 0x0A: // '\n'
							case 0x0D: // '\r'
								// key associated with an empty string element
								properties.setProperty(key, element);
								key.erase(key.begin(), key.end());
								element.erase(element.begin(), element.end());
								lexemType = BEGIN;

								if (!get(in, c))
								{
									finished = true;
								}

								break;

							default:
								element.append(1, c);

								if (!get(in, c))
								{
									finished = true;
								}

								break;
						}

						break;

					case ELEMENT_ESCAPE:
						switch (c)
						{
							case 0x74: // 't'
								element.append(1, 0x09);
								lexemType = ELEMENT;
								break;

							case 0x6E: // 'n'
								element.append(1, 0x0A);
								lexemType = ELEMENT;
								break;

							case 0x72: // 'r'
								element.append(1, 0x0D);
								lexemType = ELEMENT;
								break;

							case 0x0A: // '\n'
								lexemType = ELEMENT_CONTINUE;
								break;

							case 0x0D: // '\r'
								lexemType = ELEMENT_CONTINUE2;
								break;

							default:
								element.append(1, c);
								lexemType = ELEMENT;
								break;
						}

						if (!get(in, c))
						{
							finished = true;
						}

						break;

					case ELEMENT_CONTINUE:
						switch (c)
						{
							case 0x20: // ' '
							case 0x09: // '\t'
								if (!get(in, c))
								{
									finished = true;
								}

								break;

							default:
								lexemType = ELEMENT;
								break;
						}

						break;

					case ELEMENT_CONTINUE2:
						switch (c)
						{
							case 0x0A: // '\n'
								if (!get(in, c))
								{
									finished = true;
								}

								lexemType = ELEMENT_CONTINUE;
								break;

							default:
								lexemType = ELEMENT_CONTINUE;
								break;
						}

						break;

					case COMMENT:
						if (c == 0x0A || c == 0x0D)
						{
							lexemType = BEGIN;
						}

						if (!get(in, c))
						{
							finished = true;
						}

						break;
				}
			}

			if (!key.empty())
			{
				properties.setProperty(key, element);
			}
		}

	protected:
		static bool get(LogString& in, logchar& c)
		{
			if (in.empty())
			{
				c = 0;
				return false;
			}

			c = in[0];
			in.erase(in.begin());
			return true;
		}

		typedef enum
		{
			BEGIN,
			KEY,
			KEY_ESCAPE,
			KEY_CONTINUE,
			KEY_CONTINUE2,
			DELIMITER,
			ELEMENT,
			ELEMENT_ESCAPE,
			ELEMENT_CONTINUE,
			ELEMENT_CONTINUE2,
			COMMENT
		}
		LexemType;
};

Properties::Properties() : properties(new PropertyMap())
{
}

Properties::Properties(const Properties& other)
	: properties(new PropertyMap(*other.properties))
{
}

Properties& Properties::operator=(const Properties& other)
{
	delete this->properties;
	this->properties = new PropertyMap(*other.properties);
	return *this;
}

Properties::~Properties()
{
	delete properties;
}

LogString Properties::setProperty(const LogString& key, const LogString& value)
{
	return put(key, value);
}

LogString Properties::put(const LogString& key, const LogString& value)
{
	LogString oldValue((*properties)[key]);
	(*properties)[key] = value;
	return oldValue;
}

LogString Properties::getProperty(const LogString& key) const
{
	return get(key);
}

LogString Properties::get(const LogString& key) const
{
	PropertyMap::const_iterator it = properties->find(key);
	return (it != properties->end()) ? it->second : LogString();
}

void Properties::load(InputStreamPtr inStream)
{
	auto contents = InputStreamReader(inStream, CharsetDecoder::getISOLatinDecoder()).read();
	PropertyParser().parse(contents, *this);
}

std::vector<LogString> Properties::propertyNames() const
{
	std::vector<LogString> names;
	names.reserve(properties->size());

	for (auto const& item : *properties)
	{
		const LogString& key = item.first;
		names.push_back(key);
	}

	return names;
}

bool Properties::isEmpty() const
{
	return properties->empty();
}
