/*
 * 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/propertyconfigurator.h>
#include <log4cxx/asyncappender.h>
#include <log4cxx/helpers/properties.h>
#include <log4cxx/helpers/loglog.h>
#include <log4cxx/helpers/exception.h>
#include <log4cxx/logmanager.h>
#include <log4cxx/helpers/optionconverter.h>
#include <log4cxx/level.h>
#if LOG4CXX_ABI_VERSION <= 15
#include <log4cxx/defaultloggerfactory.h>
#else
#include <log4cxx/spi/loggerfactory.h>
#endif
#include <log4cxx/helpers/stringhelper.h>
#include <log4cxx/layout.h>
#include <log4cxx/config/propertysetter.h>
#include <log4cxx/helpers/stringtokenizer.h>
#include <log4cxx/helpers/transcoder.h>
#include <log4cxx/helpers/fileinputstream.h>
#include <log4cxx/helpers/loader.h>
#include <log4cxx/helpers/threadutility.h>
#include <log4cxx/helpers/singletonholder.h>
#include <log4cxx/rolling/rollingfileappender.h>

#define LOG4CXX 1
#include <log4cxx/helpers/aprinitializer.h>

using namespace LOG4CXX_NS;
using namespace LOG4CXX_NS::spi;
using namespace LOG4CXX_NS::helpers;
using namespace LOG4CXX_NS::config;
using namespace LOG4CXX_NS::rolling;

#include <log4cxx/helpers/filewatchdog.h>
namespace LOG4CXX_NS
{
class PropertyWatchdog  : public FileWatchdog
{
	public:
		PropertyWatchdog(const File& filename) : FileWatchdog(filename)
		{
		}

		/**
		Call PropertyConfigurator#doConfigure(const String& configFileName,
		const spi::LoggerRepositoryPtr& hierarchy) with the
		<code>filename</code> to reconfigure log4cxx.
		*/
		void doOnChange()
		{
			PropertyConfigurator().doConfigure(file(),
				LogManager::getLoggerRepository());
		}

		static void startWatching(const File& filename, long delay)
		{
			using WatchdogHolder = SingletonHolder<PropertyWatchdog>;
			auto pHolder = APRInitializer::getOrAddUnique<WatchdogHolder>
				( [&filename]() -> ObjectPtr
					{ return std::make_shared<WatchdogHolder>(filename); }
				);
			auto& pdog = pHolder->value();
			pdog.setFile(filename);
			pdog.setDelay(0 < delay ? delay : FileWatchdog::DEFAULT_DELAY);
			pdog.start();
		}
};
}

IMPLEMENT_LOG4CXX_OBJECT(PropertyConfigurator)

using RegistryType = std::map<LogString, AppenderPtr>;
using RegistryPtr = std::unique_ptr<RegistryType>;

#if 15 < LOG4CXX_ABI_VERSION
struct PropertyConfigurator::PrivateData
{

	/**
	Used internally to keep track of configured appenders.
	*/
	RegistryPtr registry{ std::make_unique<RegistryType>() };

	/**
	Used to create new instances of logger
	*/
	spi::LoggerFactoryPtr loggerFactory{ std::make_shared<LoggerFactory>() };

	/**
	True if an appender was added to a logger
	*/
	bool appenderAdded{ false };
};
PropertyConfigurator::PropertyConfigurator()
	: m_priv{ std::make_unique<PrivateData>() }
#else
#define m_priv this
PropertyConfigurator::PropertyConfigurator()
	: registry(new std::map<LogString, AppenderPtr>())
	, loggerFactory(new DefaultLoggerFactory())
#endif
{
}

PropertyConfigurator::~PropertyConfigurator()
{
#if LOG4CXX_ABI_VERSION <= 15
	delete registry;
#endif
}

spi::ConfigurationStatus PropertyConfigurator::doConfigure
	( const File&                     configFileName
#if LOG4CXX_ABI_VERSION <= 15
	, spi::LoggerRepositoryPtr        repository
#else
	, const spi::LoggerRepositoryPtr& repository
#endif
	)
{
	auto result = spi::ConfigurationStatus::NotConfigured;
	if (LogLog::isDebugEnabled())
	{
		LogLog::debug(LOG4CXX_STR("Loading configuration file [")
			+ configFileName.getPath() + LOG4CXX_STR("]"));
	}
	Properties props = Configurator::properties();
	try
	{
		InputStreamPtr inputStream = InputStreamPtr( new FileInputStream(configFileName) );
		props.load(inputStream);
	}
	catch (const IOException& ex)
	{
		LogLog::error(LOG4CXX_STR("Could not load properties from [")
			+ configFileName.getPath() + LOG4CXX_STR("]"), ex);
		return result;
	}

	try
	{
		result = doConfigure(props, repository ? repository : LogManager::getLoggerRepository());
#if LOG4CXX_ABI_VERSION <= 15
		if (m_priv->registry->empty())
#else
		if (!m_priv->appenderAdded)
#endif
		{
			LogLog::warn(LOG4CXX_STR("[") + configFileName.getPath()
				+ LOG4CXX_STR("] did not add an ") + Appender::getStaticClass().getName()
				+ LOG4CXX_STR(" to a logger"));
		}
	}
	catch (const std::exception& ex)
	{
		LogLog::error(LOG4CXX_STR("Exception thrown processing [")
			+ configFileName.getPath() + LOG4CXX_STR("]: "), ex);
	}

	return result;
}

spi::ConfigurationStatus PropertyConfigurator::configure(const File& configFilename)
{
	return PropertyConfigurator().doConfigure(configFilename, LogManager::getLoggerRepository());
}

spi::ConfigurationStatus PropertyConfigurator::configure(helpers::Properties& properties)
{
	return PropertyConfigurator().doConfigure(properties, LogManager::getLoggerRepository());
}

#if LOG4CXX_ABI_VERSION <= 15
spi::ConfigurationStatus PropertyConfigurator::configureAndWatch(const File& configFilename)
{
	return configureAndWatch(configFilename, FileWatchdog::DEFAULT_DELAY);
}
#endif

spi::ConfigurationStatus PropertyConfigurator::configureAndWatch(
	const File& configFilename, long delay)
{
	spi::ConfigurationStatus stat = PropertyConfigurator().doConfigure(configFilename, LogManager::getLoggerRepository());
	PropertyWatchdog::startWatching(configFilename, delay);
	return stat;
}

spi::ConfigurationStatus PropertyConfigurator::doConfigure(helpers::Properties& properties,
	spi::LoggerRepositoryPtr hierarchy)
{
	LogString debugValue(properties.getProperty(LOG4CXX_STR("log4j.debug")));
	if (!debugValue.empty())
	{
		LogLog::setInternalDebugging(OptionConverter::toBoolean(debugValue, true));
	}

	LogString colorValue(properties.getProperty(LOG4CXX_STR("log4j.color")));
	if (!colorValue.empty())
	{
		LogLog::setColorEnabled(OptionConverter::toBoolean(colorValue, true));
	}

	LogString thresholdStr =
		OptionConverter::findAndSubst(LOG4CXX_STR("log4j.threshold"), properties);

	if (!thresholdStr.empty())
	{
		hierarchy->setThreshold(OptionConverter::toLevel(thresholdStr, Level::getAll()));
		if (LogLog::isDebugEnabled())
		{
			LogLog::debug(LOG4CXX_STR("Repository threshold =[")
				+ hierarchy->getThreshold()->toString()
				+ LOG4CXX_STR("]"));
		}
	}

	LogString threadConfigurationValue(properties.getProperty(LOG4CXX_STR("log4j.threadConfiguration")));

	if ( threadConfigurationValue == LOG4CXX_STR("NoConfiguration") )
	{
		helpers::ThreadUtility::configure( ThreadConfigurationType::NoConfiguration );
	}
	else if ( threadConfigurationValue == LOG4CXX_STR("BlockSignalsOnly") )
	{
		helpers::ThreadUtility::configure( ThreadConfigurationType::BlockSignalsOnly );
	}
	else if ( threadConfigurationValue == LOG4CXX_STR("NameThreadOnly") )
	{
		helpers::ThreadUtility::configure( ThreadConfigurationType::NameThreadOnly );
	}
	else if ( threadConfigurationValue == LOG4CXX_STR("BlockSignalsAndNameThread") )
	{
		helpers::ThreadUtility::configure( ThreadConfigurationType::BlockSignalsAndNameThread );
	}

	configureRootLogger(properties, hierarchy);
	configureLoggerFactory(properties);
	parseCatsAndRenderers(properties, hierarchy);
	LogLog::debug(LOG4CXX_STR("Finished configuring."));
#if LOG4CXX_ABI_VERSION <= 15
	auto result = m_priv->registry->empty()
#else
	auto result = !m_priv->appenderAdded
#endif
		? spi::ConfigurationStatus::NotConfigured
		: spi::ConfigurationStatus::Configured;

	if (spi::ConfigurationStatus::Configured == result)
		hierarchy->setConfigured(true);
	return result;
}

void PropertyConfigurator::configureLoggerFactory(helpers::Properties& props)
{
	LogString factoryClassName =
		OptionConverter::findAndSubst(LOG4CXX_STR("log4j.loggerFactory"), props);

	if (!factoryClassName.empty())
	{
		auto instance = OptionConverter::instantiateByClassName
			( StringHelper::trim(factoryClassName)
			, LoggerFactory::getStaticClass()
#if LOG4CXX_ABI_VERSION <= 15
			, std::make_shared<DefaultLoggerFactory>()
#else
			, std::make_shared<LoggerFactory>()
#endif
			);

		m_priv->loggerFactory = LOG4CXX_NS::cast<LoggerFactory>( instance );
		Pool p;
		PropertySetter::setProperties(m_priv->loggerFactory, props, LOG4CXX_STR("log4j.factory."), p);
	}
}

void PropertyConfigurator::configureRootLogger(helpers::Properties& props,
	spi::LoggerRepositoryPtr& hierarchy)
{
	LogString effectivePrefix(LOG4CXX_STR("log4j.rootLogger"));
	LogString value = OptionConverter::findAndSubst(effectivePrefix, props);

	if (value.empty())
	{
		effectivePrefix = LOG4CXX_STR("log4j.rootCategory");
		value = OptionConverter::findAndSubst(effectivePrefix, props);
	}

	if (value.empty())
	{
		LogLog::debug(LOG4CXX_STR("Neither 'log4j.rootLogger' or 'log4j.rootCategory' found. Is this OK?"));
	}
	else
	{
		LoggerPtr root = hierarchy->getRootLogger();
		parseLogger(props, root, effectivePrefix, LOG4CXX_STR("root"), value, true);
	}
}

void PropertyConfigurator::parseCatsAndRenderers(helpers::Properties& props,
	spi::LoggerRepositoryPtr& hierarchy)
{
	for (auto key : props.propertyNames())
	{
		auto categoryFound = (0 == key.find(LOG4CXX_STR("log4j.category.")));
		if (categoryFound || 0 == key.find(LOG4CXX_STR("log4j.logger.")))
		{
			auto prefixLength =
				( categoryFound
				? LogString(LOG4CXX_STR("log4j.category."))
				: LogString(LOG4CXX_STR("log4j.logger."))
				).length();
			auto loggerName = key.substr(prefixLength);
			auto value = OptionConverter::findAndSubst(key, props);
			auto logger = hierarchy->getLogger(loggerName, m_priv->loggerFactory);
			auto additivity = parseAdditivityForLogger(props, logger, loggerName);
			parseLogger(props, logger, key, loggerName, value, additivity);

		}
	}
}

bool PropertyConfigurator::parseAdditivityForLogger(helpers::Properties& props,
	LoggerPtr& cat, const LogString& loggerName)
{
	LogString value(OptionConverter::findAndSubst(LOG4CXX_STR("log4j.additivity.") + loggerName, props));
	// touch additivity only if necessary
	if (!value.empty())
	{
		bool additivity = OptionConverter::toBoolean(value, true);
		if (LogLog::isDebugEnabled())
		{
			LogLog::debug(LOG4CXX_STR("Setting [") + loggerName + LOG4CXX_STR("] additivity to [")
				+ (additivity ? LogString(LOG4CXX_STR("true")) : LogString(LOG4CXX_STR("false")) + LOG4CXX_STR("]")));
		}

		return additivity;
	}

	return true;
}

/**
        This method must work for the root logger as well.
*/
void PropertyConfigurator::parseLogger(
	helpers::Properties& props, LoggerPtr& logger, const LogString& /* optionKey */,
	const LogString& loggerName, const LogString& value, bool additivity)
{
	if (LogLog::isDebugEnabled())
	{
		LogLog::debug(((LogString) LOG4CXX_STR("Parsing for ["))
			+ loggerName
			+ LOG4CXX_STR("] with value=[")
			+ value + LOG4CXX_STR("]"));
	}

	// We must skip over ',' but not white space
	StringTokenizer st(value, LOG4CXX_STR(","));

	// If value is not in the form ", appender.." or "", then we should set
	// the level of the logger.
	if (!(value.find(LOG4CXX_STR(",")) == 0 || value.empty()))
	{
		// just to be on the safe side...
		if (!st.hasMoreTokens())
		{
			return;
		}

		LogString levelStr = st.nextToken();

		// If the level value is inherited, set logger level value to
		// null. We also check that the user has not specified inherited for the
		// root logger.
		if (StringHelper::equalsIgnoreCase(levelStr, LOG4CXX_STR("INHERITED"), LOG4CXX_STR("inherited"))
			|| StringHelper::equalsIgnoreCase(levelStr, LOG4CXX_STR("NULL"), LOG4CXX_STR("null")))
		{
			if (loggerName == LOG4CXX_STR("root"))
			{
				LogLog::warn(LOG4CXX_STR("Root level cannot be ") + levelStr + LOG4CXX_STR(". Ignoring directive."));
			}
			else
			{
				logger->setLevel(0);
			}
		}
		else
		{
			logger->setLevel(OptionConverter::toLevel(levelStr, Level::getDebug()));
		}
		if (LogLog::isDebugEnabled())
		{
			LogLog::debug(loggerName + LOG4CXX_STR(" level set to ") +
				logger->getEffectiveLevel()->toString());
		}

	}

	AsyncAppenderPtr async;
	auto lsAsynchronous = OptionConverter::findAndSubst(LOG4CXX_STR("log4j.asynchronous.") + loggerName, props);
	if (!lsAsynchronous.empty() && OptionConverter::toBoolean(lsAsynchronous, true))
	{
		async = std::make_shared<AsyncAppender>();
		async->setName(loggerName);
	}

	std::vector<AppenderPtr> newappenders;
	while (st.hasMoreTokens())
	{
		auto appenderName = StringHelper::trim(st.nextToken());

		if (appenderName.empty() || appenderName == LOG4CXX_STR(","))
		{
			continue;
		}

		if (LogLog::isDebugEnabled())
		{
			LogLog::debug(LOG4CXX_STR("Parsing ") + Appender::getStaticClass().getName()
				+ LOG4CXX_STR(" named [") + appenderName + LOG4CXX_STR("]"));
		}
		if (auto appender = parseAppender(props, appenderName))
		{
			newappenders.push_back(appender);
			if (log4cxx::cast<AsyncAppender>(appender)) // An explicitly configured AsyncAppender?
				async.reset(); // Not required
			if (async)
				async->addAppender(appender);
		}
	}
#if 15 < LOG4CXX_ABI_VERSION
	if (!newappenders.empty())
		m_priv->appenderAdded = true;
#endif
	if (async && !newappenders.empty())
	{
		if (LogLog::isDebugEnabled())
		{
			LogLog::debug(LOG4CXX_STR("Asynchronous logging for [")
					+ loggerName + LOG4CXX_STR("] is on"));
		}
		logger->reconfigure( {async}, additivity );
	}
	else
		logger->reconfigure( newappenders, additivity );
}

AppenderPtr PropertyConfigurator::parseAppender(
	helpers::Properties& props, const LogString& appenderName)
{
	AppenderPtr appender = registryGet(appenderName);

	if (appender != 0)
	{
		if (LogLog::isDebugEnabled())
		{
			LogLog::debug((LogString) LOG4CXX_STR("Appender [")
				+ appenderName + LOG4CXX_STR("] was already parsed."));
		}

		return appender;
	}

	// Appender was not previously initialized.
	LogString prefix = LOG4CXX_STR("log4j.appender.") + appenderName;
	LogString layoutPrefix = prefix + LOG4CXX_STR(".layout");

	std::shared_ptr<Object> obj =
		OptionConverter::instantiateByKey(
			props, prefix, Appender::getStaticClass(), 0);
	appender = LOG4CXX_NS::cast<Appender>( obj );

	// Map obsolete DailyRollingFileAppender property configuration
	if (!appender &&
		StringHelper::endsWith(OptionConverter::findAndSubst(prefix, props), LOG4CXX_STR("DailyRollingFileAppender")))
	{
		appender = std::make_shared<RollingFileAppender>();
		auto datePattern = OptionConverter::findAndSubst(prefix + LOG4CXX_STR(".datePattern"), props);
		if (!datePattern.empty())
			props.put(prefix + LOG4CXX_STR(".fileDatePattern"), datePattern);
	}

	if (!appender)
	{
		LogLog::error((LogString) LOG4CXX_STR("Could not instantiate ") + Appender::getStaticClass().getName()
			+ LOG4CXX_STR(" named [") + appenderName + LOG4CXX_STR("]"));
		return 0;
	}

	appender->setName(appenderName);

	if (appender->instanceof(OptionHandler::getStaticClass()))
	{
		Pool p;

		if (appender->requiresLayout())
		{
			LayoutPtr layout;
			std::shared_ptr<Object> obj =
				OptionConverter::instantiateByKey(
					props, layoutPrefix, Layout::getStaticClass(), 0);
			layout = LOG4CXX_NS::cast<Layout>( obj );

			if (layout != 0)
			{
				appender->setLayout(layout);
				if (LogLog::isDebugEnabled())
				{
					LogLog::debug((LogString) LOG4CXX_STR("Parsing ") + Layout::getStaticClass().getName()
						+ LOG4CXX_STR(" options for [") + appenderName + LOG4CXX_STR("]"));
				}

				PropertySetter::setProperties(layout, props, layoutPrefix + LOG4CXX_STR("."), p);
				if (LogLog::isDebugEnabled())
				{
					LogLog::debug((LogString) LOG4CXX_STR("End of parsing for [")
						+ appenderName +  LOG4CXX_STR("]"));
				}
			}
		}

		RollingFileAppenderPtr rolling = LOG4CXX_NS::cast<rolling::RollingFileAppender>(appender);
		if (rolling)
		{
			LogString rollingPolicyKey = prefix + LOG4CXX_STR(".rollingPolicy");
			if (!OptionConverter::findAndSubst(rollingPolicyKey, props).empty())
			{
				RollingPolicyPtr rollingPolicy;
				std::shared_ptr<Object> rolling_obj =
					OptionConverter::instantiateByKey(
						props, rollingPolicyKey, RollingPolicy::getStaticClass(), 0);
				rollingPolicy = LOG4CXX_NS::cast<RollingPolicy>( rolling_obj );
				if(rollingPolicy)
				{
					rolling->setRollingPolicy(rollingPolicy);

					if (LogLog::isDebugEnabled())
					{
						LogLog::debug((LogString) LOG4CXX_STR("Parsing ") + RollingPolicy::getStaticClass().getName()
							+ LOG4CXX_STR(" options for [") + appenderName + LOG4CXX_STR("]"));
					}
					PropertySetter::setProperties(rollingPolicy, props, rollingPolicyKey + LOG4CXX_STR("."), p);
				}
			}

			LogString triggeringPolicyKey = prefix + LOG4CXX_STR(".triggeringPolicy");
			if (!OptionConverter::findAndSubst(triggeringPolicyKey, props).empty())
			{
				TriggeringPolicyPtr triggeringPolicy;
				std::shared_ptr<Object> triggering_obj =
					OptionConverter::instantiateByKey(
						props, triggeringPolicyKey, TriggeringPolicy::getStaticClass(), 0);
				triggeringPolicy = LOG4CXX_NS::cast<TriggeringPolicy>( triggering_obj );
				if(triggeringPolicy)
				{
					rolling->setTriggeringPolicy(triggeringPolicy);

					if (LogLog::isDebugEnabled())
					{
						LogLog::debug((LogString) LOG4CXX_STR("Parsing ") + TriggeringPolicy::getStaticClass().getName()
							+ LOG4CXX_STR(" options for [") + appenderName + LOG4CXX_STR("]"));
					}
					PropertySetter::setProperties(triggeringPolicy, props, triggeringPolicyKey + LOG4CXX_STR("."), p);
				}
			}
		}

		PropertySetter::setProperties(appender, props, prefix + LOG4CXX_STR("."), p);
		if (LogLog::isDebugEnabled())
		{
			LogLog::debug((LogString) LOG4CXX_STR("Parsed [")
				+ appenderName + LOG4CXX_STR("] options."));
		}
	}

	registryPut(appender);

	return appender;
}

void PropertyConfigurator::registryPut(const AppenderPtr& appender)
{
	(*m_priv->registry)[appender->getName()] = appender;
}

AppenderPtr PropertyConfigurator::registryGet(const LogString& name)
{
	return (*m_priv->registry)[name];
}
