/*
 * 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/rolling/rollingfileappender.h>
#include <log4cxx/helpers/loglog.h>
#include <log4cxx/rolling/rolloverdescription.h>
#include <log4cxx/helpers/fileoutputstream.h>
#include <log4cxx/helpers/bytebuffer.h>
#include <log4cxx/helpers/optionconverter.h>
#include <log4cxx/helpers/stringhelper.h>
#include <log4cxx/rolling/fixedwindowrollingpolicy.h>
#include <log4cxx/rolling/timebasedrollingpolicy.h>
#include <log4cxx/rolling/sizebasedtriggeringpolicy.h>
#include <log4cxx/helpers/transcoder.h>
#include <log4cxx/private/rollingfileappender_priv.h>
#include <mutex>

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

#define _priv static_cast<RollingFileAppenderPriv*>(m_priv.get())

IMPLEMENT_LOG4CXX_OBJECT(RollingFileAppender)


/**
 * Construct a new instance.
 */
RollingFileAppender::RollingFileAppender()
	: FileAppender(std::make_unique<RollingFileAppenderPriv>())
{
}

RollingFileAppender::RollingFileAppender( std::unique_ptr<RollingFileAppenderPriv> priv )
	: FileAppender(std::move(priv))
{
}

void RollingFileAppender::setOption(const LogString& option, const LogString& value)
{
	if (StringHelper::equalsIgnoreCase(option,
			LOG4CXX_STR("MAXFILESIZE"), LOG4CXX_STR("maxfilesize"))
		|| StringHelper::equalsIgnoreCase(option,
			LOG4CXX_STR("MAXIMUMFILESIZE"), LOG4CXX_STR("maximumfilesize")))
	{
		setMaxFileSize(value);
	}
	else if (StringHelper::equalsIgnoreCase(option,
			LOG4CXX_STR("MAXBACKUPINDEX"), LOG4CXX_STR("maxbackupindex"))
		|| StringHelper::equalsIgnoreCase(option,
			LOG4CXX_STR("MAXIMUMBACKUPINDEX"), LOG4CXX_STR("maximumbackupindex")))
	{
		setMaxBackupIndex(StringHelper::toInt(value));
	}
	else if (StringHelper::equalsIgnoreCase(option,
			LOG4CXX_STR("FILEDATEPATTERN"), LOG4CXX_STR("filedatepattern")))
	{
		setDatePattern(value);
	}
	else
	{
		FileAppender::setOption(option, value);
	}
}

int RollingFileAppender::getMaxBackupIndex() const
{
	int result = 1;
	if (auto fwrp = LOG4CXX_NS::cast<FixedWindowRollingPolicy>(_priv->rollingPolicy))
		result = fwrp->getMaxIndex();
	return result;
}

void RollingFileAppender::setMaxBackupIndex(int maxBackups)
{
	auto fwrp = LOG4CXX_NS::cast<FixedWindowRollingPolicy>(_priv->rollingPolicy);
	if (!fwrp)
	{
		fwrp = std::make_shared<FixedWindowRollingPolicy>();
		fwrp->setFileNamePattern(getFile() + LOG4CXX_STR(".%i"));
		_priv->rollingPolicy = fwrp;
	}
	fwrp->setMaxIndex(maxBackups);
}

size_t RollingFileAppender::getMaximumFileSize() const
{
	size_t result = 10 * 1024 * 1024;
	if (auto sbtp = LOG4CXX_NS::cast<SizeBasedTriggeringPolicy>(_priv->triggeringPolicy))
		result = sbtp->getMaxFileSize();
	return result;
}

void RollingFileAppender::setMaximumFileSize(size_t maxFileSize)
{
	auto sbtp = LOG4CXX_NS::cast<SizeBasedTriggeringPolicy>(_priv->triggeringPolicy);
	if (!sbtp)
	{
		sbtp = std::make_shared<SizeBasedTriggeringPolicy>();
		_priv->triggeringPolicy = sbtp;
	}
	sbtp->setMaxFileSize(maxFileSize);
}

void RollingFileAppender::setMaxFileSize(const LogString& value)
{
	setMaximumFileSize(OptionConverter::toFileSize(value, long(getMaximumFileSize() + 1)));
}

LogString RollingFileAppender::makeFileNamePattern(const LogString& datePattern)
{
	LogString result(getFile());
	bool inLiteral = false;
	bool inPattern = false;

	for (size_t i = 0; i < datePattern.length(); i++)
	{
		if (datePattern[i] == 0x27 /* '\'' */)
		{
			inLiteral = !inLiteral;

			if (inLiteral && inPattern)
			{
				result.append(1, (logchar) 0x7D /* '}' */);
				inPattern = false;
			}
		}
		else
		{
			if (!inLiteral && !inPattern)
			{
				const logchar dbrace[] = { 0x25, 0x64, 0x7B, 0 }; // "%d{"
				result.append(dbrace);
				inPattern = true;
			}

			result.append(1, datePattern[i]);
		}
	}

	if (inPattern)
	{
		result.append(1, (logchar) 0x7D /* '}' */);
	}
	return result;
}

void RollingFileAppender::setDatePattern(const LogString& newPattern)
{
	auto tbrp = LOG4CXX_NS::cast<TimeBasedRollingPolicy>(_priv->rollingPolicy);
	if (!tbrp)
	{
		tbrp = std::make_shared<TimeBasedRollingPolicy>();
		_priv->rollingPolicy = tbrp;
	}
	tbrp->setFileNamePattern(makeFileNamePattern(newPattern));
}

/**
 * Prepare instance of use.
 */
void RollingFileAppender::activateOptions( LOG4CXX_ACTIVATE_OPTIONS_FORMAL_PARAMETERS )
{
	if (_priv->activateOptions())
	{
		FileAppender::activateOptionsInternal();
	}
}

bool RollingFileAppender::RollingFileAppenderPriv::activateOptions()
{
	bool result = false;
	if (!this->rollingPolicy)
	{
		LogLog::warn(LOG4CXX_STR("No rolling policy configured for the appender named [")
			+ this->name + LOG4CXX_STR("]."));
		auto fwrp = std::make_shared<FixedWindowRollingPolicy>();
		fwrp->setFileNamePattern(this->fileName + LOG4CXX_STR(".%i"));
		this->rollingPolicy = fwrp;
	}
	else if (auto fwrp = LOG4CXX_NS::cast<FixedWindowRollingPolicy>(this->rollingPolicy))
	{
		// Was fwrp created to store the maximum index before the file name was set?
		if (fwrp->getFileNamePattern() == LOG4CXX_STR(".%i"))
			fwrp->setFileNamePattern(this->fileName + LOG4CXX_STR(".%i"));
	}

	//
	//  if no explicit triggering policy and rolling policy is both.
	//
	if (!this->triggeringPolicy)
	{
		TriggeringPolicyPtr trig = LOG4CXX_NS::cast<TriggeringPolicy>(this->rollingPolicy);

		if (trig != NULL)
		{
			this->triggeringPolicy = trig;
		}
	}

	if (!this->triggeringPolicy)
	{
		LogLog::warn(LOG4CXX_STR("No triggering policy configured for the appender named [")
			+ this->name + LOG4CXX_STR("]."));
		this->triggeringPolicy = std::make_shared<SizeBasedTriggeringPolicy>();
	}

	{
		std::lock_guard<std::recursive_mutex> lock(this->mutex);
		this->triggeringPolicy->activateOptions();
		this->rollingPolicy->activateOptions();

		try
		{
			RolloverDescriptionPtr rollover1 =
				this->rollingPolicy->initialize(this->fileName, this->fileAppend);

			if (rollover1 != NULL)
			{
				ActionPtr syncAction(rollover1->getSynchronous());

				if (syncAction != NULL)
				{
					syncAction->execute();
				}

				this->fileName = rollover1->getActiveFileName();
				this->fileAppend = rollover1->getAppend();

				//
				//  async action not yet implemented
				//
				ActionPtr asyncAction(rollover1->getAsynchronous());

				if (asyncAction != NULL)
				{
					asyncAction->execute();
				}
			}

			File activeFile;
			activeFile.setPath(this->fileName);

			if (this->fileAppend)
			{
				this->fileLength = activeFile.length();
			}
			else
			{
				this->fileLength = 0;
			}

			result = true;
		}
		catch (std::exception& ex)
		{
			LogLog::warn(LOG4CXX_STR("Exception activating RollingFileAppender ") + this->fileName, ex);
		}
	}
	return result;
}

/**
   Implements the usual roll over behaviour.

   <p>If <code>MaxBackupIndex</code> is positive, then files
   {<code>File.1</code>, ..., <code>File.MaxBackupIndex -1</code>}
   are renamed to {<code>File.2</code>, ...,
   <code>File.MaxBackupIndex</code>}. Moreover, <code>File</code> is
   renamed <code>File.1</code> and closed. A new <code>File</code> is
   created to receive further log output.

   <p>If <code>MaxBackupIndex</code> is equal to zero, then the
   <code>File</code> is truncated with no backup files created.

 * @return true if rollover performed.
 */
bool RollingFileAppender::rollover()
{
	std::lock_guard<std::recursive_mutex> lock(_priv->mutex);
	return rolloverInternal();
}
#if LOG4CXX_ABI_VERSION <= 15
bool RollingFileAppender::rollover(Pool& )
{
	return rollover();
}
#endif

bool RollingFileAppender::rolloverInternal()
{
	//
	//   can't roll without a policy
	//
	if (_priv->rollingPolicy != NULL)
	{
		{
				try
				{
					RolloverDescriptionPtr rollover1(_priv->rollingPolicy->rollover(this->getFile(), this->getAppend()));

					if (rollover1 != NULL)
					{
						if (rollover1->getActiveFileName() == getFile())
						{
							_priv->close();

							bool success = true;

							if (rollover1->getSynchronous() != NULL)
							{
								success = false;

								try
								{
									success = rollover1->getSynchronous()->execute();
								}
								catch (std::exception& ex)
								{
									LogString msg(LOG4CXX_STR("Rollover of ["));
									msg.append(getFile());
									msg.append(LOG4CXX_STR("] failed"));
									_priv->errorHandler->error(msg, ex, 0);
								}
							}

							bool appendToExisting = true;
							if (success)
							{
								appendToExisting = rollover1->getAppend();
								if (appendToExisting)
								{
									_priv->fileLength = File().setPath(rollover1->getActiveFileName()).length();
								}
								else
								{
									_priv->fileLength = 0;
								}

								ActionPtr asyncAction(rollover1->getAsynchronous());

								if (asyncAction != NULL)
								{
									try
									{
										asyncAction->execute();
									}
									catch (std::exception& ex)
									{
										LogString msg(LOG4CXX_STR("Rollover of ["));
										msg.append(getFile());
										msg.append(LOG4CXX_STR("] failed"));
										_priv->errorHandler->error(msg, ex, 0);
									}
								}
							}
							setFileInternal(rollover1->getActiveFileName(), appendToExisting, _priv->bufferedIO, _priv->bufferSize);
						}
						else
						{
							_priv->close();
							setFileInternal(rollover1->getActiveFileName());
							// Call activateOptions to create any intermediate directories(if required)
							FileAppender::activateOptionsInternal();
							OutputStreamPtr os = std::make_shared<FileOutputStream>
									( rollover1->getActiveFileName()
									, rollover1->getAppend()
									);
							_priv->setWriter(createWriter(os));

							bool success = true;

							if (rollover1->getSynchronous() != NULL)
							{
								success = false;

								try
								{
									success = rollover1->getSynchronous()->execute();
								}
								catch (std::exception& ex)
								{
									LogString msg(LOG4CXX_STR("Rollover of ["));
									msg.append(getFile());
									msg.append(LOG4CXX_STR("] failed"));
									_priv->errorHandler->error(msg, ex, 0);
								}
							}

							if (success)
							{
								if (rollover1->getAppend())
								{
									_priv->fileLength = File().setPath(rollover1->getActiveFileName()).length();
								}
								else
								{
									_priv->fileLength = 0;
								}

								ActionPtr asyncAction(rollover1->getAsynchronous());

								if (asyncAction != NULL)
								{
									asyncAction->execute();
								}
							}

							_priv->writeHeader();
						}
						return true;
					}
				}
				catch (std::exception& ex)
				{
					LogString msg(LOG4CXX_STR("Rollover of ["));
					msg.append(getFile());
					msg.append(LOG4CXX_STR("] failed"));
					_priv->errorHandler->error(msg, ex, 0);
				}
		}
	}

	return false;
}
#if LOG4CXX_ABI_VERSION <= 15
bool RollingFileAppender::rolloverInternal(Pool&)
{
	return rolloverInternal();
}
#endif

/**
 * {@inheritDoc}
*/
void RollingFileAppender::subAppend( LOG4CXX_APPEND_FORMAL_PARAMETERS )
{
	// The rollover check must precede actual writing. This is the
	// only correct behavior for time driven triggers.
	if (
		_priv->triggeringPolicy->isTriggeringEvent(
			this, event, getFile(), getFileLength()))
	{
		//
		//   wrap rollover request in try block since
		//    rollover may fail in case read access to directory
		//    is not provided.  However appender should still be in good
		//     condition and the append should still happen.
		try
		{
			_priv->_event = event;
			rolloverInternal();
		}
		catch (std::exception& ex)
		{
			LogString msg(LOG4CXX_STR("Rollover of ["));
			msg.append(getFile());
			msg.append(LOG4CXX_STR("] failed"));
			_priv->errorHandler->error(msg, ex, 0);
		}
	}

	FileAppender::subAppend( LOG4CXX_APPEND_PARAMETERS );
}

/**
 * TThe policy that implements the scheme for rolling over a log file.
 */
RollingPolicyPtr RollingFileAppender::getRollingPolicy() const
{
	return _priv->rollingPolicy;
}

/**
 * The policy that determine when to trigger a log file rollover.
 */
TriggeringPolicyPtr RollingFileAppender::getTriggeringPolicy() const
{
	return _priv->triggeringPolicy;
}

/**
 * Set the scheme for rolling over log files.
 */
void RollingFileAppender::setRollingPolicy(const RollingPolicyPtr& policy)
{
	_priv->rollingPolicy = policy;
}

/**
 * Set policy that determine when to trigger a log file rollover.
 */
void RollingFileAppender::setTriggeringPolicy(const TriggeringPolicyPtr& policy)
{
	_priv->triggeringPolicy = policy;
}

/**
 * Close appender.  Waits for any asynchronous file compression actions to be completed.
 */
void RollingFileAppender::close()
{
	FileAppender::close();
}

namespace LOG4CXX_NS
{
namespace rolling
{
/**
 * Wrapper for OutputStream that will report all write
 * operations back to this class for file length calculations.
 */
class CountingOutputStream : public OutputStream
{
		/**
		 * Wrapped output stream.
		 */
	private:
		OutputStreamPtr os;

		/**
		 * Rolling file appender to inform of stream writes.
		 */
		RollingFileAppender* rfa;

	public:
		/**
		 * Constructor.
		 * @param os output stream to wrap.
		 * @param rfa rolling file appender to inform.
		 */
		CountingOutputStream
			( const OutputStreamPtr& os1
			, RollingFileAppender* rfa1
			)
			: os(os1)
			, rfa(rfa1)
		{
		}

		/**
		 * {@inheritDoc}
		 */
		void close( LOG4CXX_CLOSE_OUTPUT_STREAM_FORMAL_PARAMETERS ) override
		{
			os->close();
			rfa = 0;
		}

		/**
		 * {@inheritDoc}
		 */
		void flush( LOG4CXX_FLUSH_OUTPUT_STREAM_FORMAL_PARAMETERS ) override
		{
			os->flush();
		}

		/**
		 * {@inheritDoc}
		 */
		void write( LOG4CXX_WRITE_OUTPUT_STREAM_FORMAL_PARAMETERS ) override
		{
			os->write(buf);

			if (rfa != 0)
			{
				rfa->incrementFileLength(buf.limit());
			}
		}
};
}
}

/**
   Returns an OutputStreamWriter when passed an OutputStream.  The
   encoding used will depend on the value of the
   <code>encoding</code> property.  If the encoding value is
   specified incorrectly the writer will be opened using the default
   system encoding (an error message will be printed to the loglog.
 @param os output stream, may not be null.
 @return new writer.
 */
WriterPtr RollingFileAppender::createWriter(LOG4CXX_16_CONST OutputStreamPtr& os)
{
	OutputStreamPtr cos = std::make_shared<CountingOutputStream>(os, this);
	return FileAppender::createWriter(cos);
}

/**
 * Get byte length of current active log file.
 * @return byte length of current active log file.
 */
size_t RollingFileAppender::getFileLength() const
{
	return _priv->fileLength;
}

/**
 * Increments estimated byte length of current active log file.
 * @param increment additional bytes written to log file.
 */
void RollingFileAppender::incrementFileLength(size_t increment)
{
	_priv->fileLength += increment;
}
