/*
* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description:
*
*/
#include <pathinfo.h>
#include <pathconfiguration.hrh>
#include <coemain.h> // For CCoeEnv
#include <QDate>

#include "cxefilenamegeneratorsymbian.h"
#include "cxeengine.h"
#include "cxenamespace.h"
#include "cxutils.h"
#include "cxesysutil.h"
#include "cxesettings.h"
#include "cxeerror.h"
#include "cxeerrormappingsymbian.h"


using namespace Cxe;

// ===========================================================================
// Local constants

static const int KMultipleFolderNumberChars = 2;
static const int KMaxMonthFolders           = 260;
static const int KBase10                    = 10;
static const int KMaxFilesPerFolder         = 100;

const char* CAMERA_FOLDER   = "Camera";
const char* FOLDER_SEPARATOR   = "\\";
const char* WILDCARD_CHARACTER = "?";

const char CHAR_OFFSET = 'A';
const char DIGIT_OFFSET = '0';
const char MAX_CHAR = 'Z';

const TInt64 KMinRequiredSpaceImage = 2000000;
const TInt64 KMinRequiredSpaceVideo = 4000000;



// ===========================================================================
// Local functions

/**
* Convert QString to TPtrC16.
*/
inline TPtrC16 convertToTPtrC16(const QString& string)
{
    return TPtrC16(reinterpret_cast<const TUint16*>(string.utf16()));
}


// ===========================================================================
// Class functions

/**
 * Constructor
 */
CxeFilenameGeneratorSymbian::CxeFilenameGeneratorSymbian(CxeSettings &settings, CameraMode mode)
       : mFs(CCoeEnv::Static()->FsSession()),
         mSettings(settings),
         mCurrentMode(mode)
{
    CX_DEBUG_ENTER_FUNCTION();

    // Set default values (used in case of error retrieving values)
    mCurrentMonth = "";
    mMonthCounterImage = -1;
    mMonthCounterVideo = -1;
    mImageCounter = 0;
    mVideoCounter = 0;

    // Retrieve last used counter values from settings
    mSettings.get(CxeSettingIds::FNAME_MONTH_FOLDER, mCurrentMonth);
    mSettings.get(CxeSettingIds::FNAME_IMAGE_COUNTER, mImageCounter);
    mSettings.get(CxeSettingIds::FNAME_VIDEO_COUNTER, mVideoCounter);

    CX_DEBUG_EXIT_FUNCTION();
}

/**
 * Destructor
 */
CxeFilenameGeneratorSymbian::~CxeFilenameGeneratorSymbian()
{
    CX_DEBUG_ENTER_FUNCTION();

    CX_DEBUG_EXIT_FUNCTION();
}

/**
 * This must be called for every burst capture.
 */
void CxeFilenameGeneratorSymbian::startNewImageFilenameSequence()
{
    CX_DEBUG_ENTER_FUNCTION();

    /*  NOTES
    When the client calls this function, we should start a new "sequence" of filenames...
    What I mean here is that for burst capture, we have filename sequences like...
        Filename1
        Filename1(002)
        Filename1(003)
        and so on...

    So this function says "Ok, I'm starting a new capture sequence here... so reset your variables"
    This should be done below...
    Development efforts need to be aware of Edrive vs. Cdrive, and also multiple drive support.
    */

    CX_DEBUG_EXIT_FUNCTION();
}

/**
 * Generates the next file name in the sequence.
 * @param   filename: A QString reference to hold the filename
 * @return  error id, Id::None if no error
 */
CxeError::Id CxeFilenameGeneratorSymbian::nextImageFilenameInSequence(QString &qfName, const QString &fileExt)
{
    CX_DEBUG_IN_FUNCTION();

    /** @todo: When the client calls this function, the next filename in the
     * sequence is retrieved. The entire path should be included... e.g
     * C//PHOTOS//SOMETHING//HELLO//Filename001.jpg
     */
    return generateFilename(qfName, fileExt);
}

/**
 * Generates image/video file name depending on the current active mode.
 * @param   filename: A QString reference to hold the filename
 * @return  error id, CxeError::None if no error.
 */
CxeError::Id CxeFilenameGeneratorSymbian::generateFilename(QString &qfName, const QString &fileExt)
{
    CX_DEBUG_ENTER_FUNCTION();

    // Make sure that the path for images/videos exists
    QString path;
    int err = selectFolder(path);

    // Continue generating filename, if no errors encountered yet
    if (!err) {
        // Generate and append the file name to construct the full path
        QString fileName;
        err = generateUniqueFileName(path, fileName, fileExt);
        if (!err)  {
            qfName = path + fileName;
        }
    }

    CX_DEBUG_EXIT_FUNCTION();
    return CxeErrorHandlingSymbian::map(err);
}

/**
 * Generates a unique file name in the given path.
 * @param filePath          Path for the file (reference)
 * @param fileName          Name of the file (reference)
 * @return                  Symbian error code (used internally)
 */
int CxeFilenameGeneratorSymbian::generateUniqueFileName(QString &filePath, QString &fileName, const QString &ext)
{
    CX_DEBUG_ENTER_FUNCTION();

    // At this point the path must exist.
    // Remote possibility / security threat that it doesn't exist or is not a folder.
    TEntry entry;
    int ferr = mFs.Entry(convertToTPtrC16(filePath), entry);
    if (ferr != KErrNone || !entry.IsDir()) {
        return ferr;
    }

    // Initialize counters and file extension
    int *fileCounter = &mImageCounter;

    if (mCurrentMode==VideoMode) {
        fileCounter  = &mVideoCounter;
    }

    QString fnFormat("%1%2%3");
    QString number;

    // get the file name suffix
    QString baseFileName;
    mSettings.get(CxeSettingIds::FNAME_FOLDER_SUFFIX, baseFileName);

    while (true) {
        // Generate new name
        number = number.sprintf("%04d", *fileCounter);
        fileName = fnFormat.arg(number, baseFileName, ext);

        CX_DEBUG(("Checking filename [%s]", fileName.toAscii().constData()));

        // Check if it exists
        ferr = checkExistence(filePath + fileName);
        if (ferr == KErrNone) {
            // Filename has already been used in this location, try the next number
            CX_DEBUG(("File exists already"));
            (*fileCounter)++;
        } else if (ferr == KErrNotFound) {
            // No file with this name, use it.
            CX_DEBUG(("Filename free, using it"));
            ferr = KErrNone;
            break;
        } else {
            // Filename could not be reserved. Log actual error and report as general error.
            CX_DEBUG(("[WARNING] Error %d generating filename!", ferr));
            ferr = KErrGeneral;
            break;
        }
    }

    // Increment counter if image mode. Video counter is incremented when
    // video has been actually recorded
    if (mCurrentMode == ImageMode) {
        raiseCounterValue();
    }

    CX_DEBUG_EXIT_FUNCTION();
    return ferr;
}

/**
 * Computes the suffix to be used in destination folder name.
 */
void CxeFilenameGeneratorSymbian::computeFolderSuffix(int folderNumber, QString &suffix)
{
    CX_DEBUG_ENTER_FUNCTION();

    // Clear any old content.
    suffix = "";

    // Example:
    // if folderNumber is 15
    // 15/10 = 1 (int)
    // A + 1  = B
    suffix += (CHAR_OFFSET + folderNumber/KBase10);
    // 15%10 = 5
    // 0 + 5 = 5
    // => suffix = B5
    suffix += (DIGIT_OFFSET + folderNumber%KBase10);
    CX_DEBUG_EXIT_FUNCTION();
}

/**
 * Compute the month counter value (0 to 259 => A0 to Z9).
 * @param
 * @return   Symbian error code (internal to class)
 */
int CxeFilenameGeneratorSymbian::computeMonthCounter(QString &path, QString& monthFolder, int &monthCounter)
{
    CX_DEBUG_ENTER_FUNCTION();
    bool done = false;

    // Query the month folder: path\YYYYMM??
    QString queryPath = path + monthFolder;
    for (int charCount = 0; charCount < KMultipleFolderNumberChars; charCount++) {
        queryPath += WILDCARD_CHARACTER;
    }
    CX_DEBUG(("Listing folders with wildcards: [%s]", queryPath.toAscii().constData()));

    // Get a list of folders for this month, sorted in descending alphabetical order.
    // The first entry should be the latest used folder.
    CDir* dirList;
    int err = mFs.GetDir(convertToTPtrC16(queryPath),
                         KEntryAttMatchExclusive|KEntryAttDir,
                         ESortByName|EDescending,
                         dirList);

    // On errors (or empty list of folders), let's default to zero.
    // Errors here should be recoverable when we start to
    // count files within current directory.
    monthCounter = 0;

    // If directory listing worked fine, then figure out the last month folder number
    if (err == KErrNone) {
        int monthFolderCount = dirList->Count();
        CX_DEBUG(("Matching folder count: %d", monthFolderCount));
        int index = 0;
        // Look through the list of folders in the month for the highest numbered folder
        // with the format YYYYMMAX Where YYYY is the year MM is the month A is an alphabetical
        // character in the range a-z or A-Z and X is a digit 0-9
        while ( index < monthFolderCount && !done ) {
            // The list is sorted in descending order. Get the last 2 characters from
            // the first directory in the list these indicate the highest folder number
            TPtrC name = ( *dirList )[index].iName;
            int nameLength = name.Length();

            // Check the first character is in the range a-z or A-Z
            // and the second character is in the range 0-9
            TChar firstChar = name[nameLength - KMultipleFolderNumberChars];
            firstChar.UpperCase();
            TChar secondChar = name[nameLength - 1];
            int secondCharVal = secondChar.GetNumericValue();

            // If 1st character is NOT in the range(A-Z) then disregard this folder
            if (firstChar < CHAR_OFFSET || firstChar > MAX_CHAR) {
                done = false;
            }
            // If 2nd character is NOT in the range(0-9) then disregard this folder
            else if (secondCharVal < 0 || secondCharVal > KBase10-1) {
                done = false;
            }
            else {
                done = true;
            }

            // If no problems were encountered, then calculate the folder number
            if (done) {
                // 10's part of folder number is represented by characters A-Z
                // convert the character into a decimal value (*10) and add on the units.
                monthCounter = ((int)firstChar-(int)CHAR_OFFSET)*KBase10 + secondCharVal;
                CX_DEBUG(("Calculated month counter value: %d", monthCounter));
                // Make sure that month counter doesn't exceed Z9
                if (monthCounter >= KMaxMonthFolders) {
                    monthCounter = KMaxMonthFolders - 1;
                }
            }
            index++;
        }
    } else {
        CX_DEBUG(("[WARNING] Error %d listing month folders!", err));
    }

    delete dirList;
    CX_DEBUG_EXIT_FUNCTION();
    return err;
}

/**
 * Checks that the month folder exists: creates the folder if required.
 * Returns error code if the folder can't be created.
 */
int CxeFilenameGeneratorSymbian::selectFolder(QString &suggestedPath)
{
    CX_DEBUG_ENTER_FUNCTION();

    // Compose the path string and select counter based on mode.
    QString basePath = "%1%2\\";
    int *monthCounter = &mMonthCounterImage;

    if (ImageMode == mCurrentMode) {
        basePath = basePath.arg(mImagesPath, mCurrentMonth);
    }
    else { // VideoMode
        basePath = basePath.arg(mVideosPath, mCurrentMonth);
        monthCounter = &mMonthCounterVideo;
    }
    CX_DEBUG(("Base path [%s]", basePath.toAscii().constData()));

    QString suffix;
    QString newPath;
    int status(KErrNone);
    bool created(false);

    while (true) {
        // Construct the complete path
        computeFolderSuffix(*monthCounter, suffix);
        newPath = basePath + mCurrentMonth + suffix + FOLDER_SEPARATOR;

        // Check if the folder exists, and create if not.
        CX_DEBUG(("Checking new path [%s] ..", newPath.toAscii().constData()));
        status = ensureExists(newPath, created);

        // Check if the folder can be used.
        if (status == KErrNone) {
            // If the folder was newly created, it's empty and can be used.
            // If the folder is last available month folder,
            // we need to use it even if it has "too many" files.
            if (created || *monthCounter >= (KMaxMonthFolders-1)) {
                CX_DEBUG(("Newly created folder, or the last month folder available, using it"));
                suggestedPath = newPath;
                break;
            } else {
                // For other folders, if the folder already exists then make sure
                // that it doesn't contain maximum number of files already.
                CX_DEBUG(("Folder already existed, check the amount of files.."));
                CDir* fileList;
                status = mFs.GetDir(convertToTPtrC16(newPath),
                                    KEntryAttMaskSupported,
                                    ESortNone,
                                    fileList);
                CX_DEBUG(("Folder contains %d files", fileList->Count()));

                bool spaceRemaining(fileList->Count() < KMaxFilesPerFolder);

                delete fileList;
                fileList = NULL;

                if (status == KErrNone && spaceRemaining) {
                    // Not too many files on the folder, use it.
                    CX_DEBUG(("File count acceptable, using this folder"));
                    suggestedPath = newPath;
                    break;
                } else {
                    // Need to check the next folder, this one is full,
                    // or we had error listing it's contents.
                    CX_DEBUG(("Folder full, continue search"));
                    (*monthCounter)++;
                }
            }
        } else {
            // Unknown error encountered.
            // LOG the error and use the basePath!
            // Base path's existence has been checked when init was called.
            CX_DEBUG(("[WARNING] - error %d encountered. Using base path as fallback.", status));
            suggestedPath = basePath;
            break;
        }
    }

    CX_DEBUG_EXIT_FUNCTION();

    // We fallback to basePath in case of unknown errors,
    // so no error will be reported here.
    return KErrNone;
}



/**
 * Initializes the month folders.
 * @return Symbian error code (internal to class)
 */
int CxeFilenameGeneratorSymbian::initMonthFolders()
{
    CX_DEBUG_ENTER_FUNCTION();

    // Month folder: YYYYMM, with suffix: YYYYMMXX
    QString monthFolder = QDate::currentDate().toString("yyyyMM");

    // If the month folder name is different from the last used month folder name
    // this indicates that a new month has been started.
    if ( monthFolder.compare(mCurrentMonth) != 0 ) {
        resetCounters(monthFolder);
    }

    // Check/create that the month folder exists
    int *monthCounter = &mMonthCounterImage;
    QString path = "%1%2\\";
    if (ImageMode==mCurrentMode) {
        path = path.arg(mImagesPath, monthFolder);
    }
    else {
        path = path.arg(mVideosPath, monthFolder);
        monthCounter = &mMonthCounterVideo;
    }

    CX_DEBUG(("Check if exists, create if not [%s]", path.toAscii().constData()));
    bool created(false);
    int status(ensureExists(path, created));

    if (status == KErrNone) {
        if (created) {
            // New folder, new month -> counter starts from zero.
            *monthCounter = 0;
        } else {
            // If the month counter is un-initialised it needs to be set up.
            if (*monthCounter < 0) {
                status = computeMonthCounter(path, monthFolder, *monthCounter);
                if (status != KErrNone) {
                    CX_DEBUG(("[WARNING] - Error setting month counter: %d", status));
                    *monthCounter = 0;
                }
            }
        }
    } else {
        //! @todo: Review error handling
        // Report error - can't continue without 'camera' and camera\YYYYMM folders
        CX_DEBUG(("[FATAL] - Could not create month folder, error %d", status));
    }

    CX_DEBUG_EXIT_FUNCTION();
    return status;
}

/**
 * Checks if the passed amount of disk space is available on the passed drive.
 * @return   true if space available, false otherwise
 */
bool CxeFilenameGeneratorSymbian::spaceAvailable(int &driveIndex, TInt64 minSpaceInBytes)
{
    CX_DEBUG_ENTER_FUNCTION();

    TVolumeInfo vInfo;

    // Get the drive/volume details
    int status(KErrNone);
    driveIndex = CxeSysUtil::getCameraDrive(mFs);
    if (driveIndex >= 0) {
        status = mFs.Volume(vInfo, driveIndex);
    }

    CX_DEBUG_EXIT_FUNCTION();
    return (status == KErrNone) ? vInfo.iFree >= minSpaceInBytes : false;

}

/**
 * Selects the drive to use based on preference and availability.
 * @param   index of the drive to be used (reference)
 * @return  Symbian error code (internal to class)
 */
int CxeFilenameGeneratorSymbian::selectDrive(int &drive)
{
    CX_DEBUG_ENTER_FUNCTION();
    int err = KErrNone;
    TInt64 minDiskSpace = KMinRequiredSpaceImage;

	if (Cxe::VideoMode == mCurrentMode) {
	    minDiskSpace = KMinRequiredSpaceVideo;
    }

    // Check the available space.
    // Drive index is set here also.
    if ( !spaceAvailable(drive, minDiskSpace)) {
        // All drives are full or inaccessible
        err = KErrDiskFull;
    }

    CX_DEBUG_EXIT_FUNCTION();
    return err;
}

/**
 * Initializes the value of base path for the given mode and drive.
 */
void CxeFilenameGeneratorSymbian::initBasePath(QString &path, int drive)
{
    CX_DEBUG_ENTER_FUNCTION();

    // Get the root path for the given drive.
    TFileName tPath;
    PathInfo::GetRootPath(tPath, drive);

    if (VideoMode == mCurrentMode) {
        tPath.Append( PathInfo::VideosPath() );
    }
    else {
        tPath.Append( PathInfo::ImagesPath() );
    }

    //! @todo: Fetch localized Camera folder name, if/when applicable.
    path = QString::fromUtf16(tPath.Ptr(), tPath.Length());
    path = path + CAMERA_FOLDER + FOLDER_SEPARATOR;

    CX_DEBUG(("Path: %s", path.toAscii().constData()));
    CX_DEBUG_EXIT_FUNCTION();
}

/**
 * Resets the image and video counters used for file name generation.
 */
void CxeFilenameGeneratorSymbian::resetCounters(QString &monthFolder)
{
    CX_DEBUG_ENTER_FUNCTION();

    mCurrentMonth = monthFolder;
    mMonthCounterImage = 0;
    mMonthCounterVideo = 0;

    // Save the setting values
    // Errors (if any) encountered here are not handled, since these indicate
    // that settings are'nt saved. If settings are not fetched next time,
    // then default settings are used.
    mSettings.set(CxeSettingIds::FNAME_MONTH_FOLDER, mCurrentMonth);

    CX_DEBUG_EXIT_FUNCTION();
}

/**
 * Initializes the base folder names and counters
 * NB: Video mode init requires file name. So this should be called
 * before init is called for image/video capture controls.
 * Initializes only for appropriate mode (image/video).
 */
CxeError::Id CxeFilenameGeneratorSymbian::init(Cxe::CameraMode mode)
{
    CX_DEBUG_ENTER_FUNCTION();

    mCurrentMode = mode;
    int err = KErrNone;
    bool initialized = (mode==ImageMode && !mImagesPath.isEmpty()) ||
                       (mode==VideoMode && !mVideosPath.isEmpty());

    if (!initialized) {
        // Select a drive based on available disk space
        int drive;
        err = selectDrive(drive);
        if (!err) {
            if (mode == ImageMode) {
                initBasePath(mImagesPath, drive);
            }
            else {
                initBasePath(mVideosPath, drive);
            }

            // Initialize the month folders and counters so that
            // file names can be generated quickly when requested.
            err = initMonthFolders();
        }
    }

    CX_DEBUG_EXIT_FUNCTION();
    return CxeErrorHandlingSymbian::map(err);
}

/**
 * Raises file name counter value by one
 */
void CxeFilenameGeneratorSymbian::raiseCounterValue()
{
    if (VideoMode==mCurrentMode) {
        mSettings.set(CxeSettingIds::FNAME_VIDEO_COUNTER, ++mVideoCounter);
    }
    else {
        mSettings.set(CxeSettingIds::FNAME_IMAGE_COUNTER, ++mImageCounter);
    }
}

/**
 * Check if the given file or directory exists.
 * @return KErrNone, if given file/directory exists, KErrNotFound if not.
 *         Other Symbian error code on "real" errors.
 */
int CxeFilenameGeneratorSymbian::checkExistence(const QString& path)
{
    CX_DEBUG_ENTER_FUNCTION();

    unsigned int placeHolder(0);
    int status(mFs.Att(convertToTPtrC16(path), placeHolder));

    CX_DEBUG_EXIT_FUNCTION();
    return status;
}


/**
 * Check if directory exists and create if not.
 * @param created When returned, holds if the folder needed to be created or not.
 * @return KErrNone, if folder existed or was created successfully.
 *         Other Symbian error code if there were errors.
 */
int CxeFilenameGeneratorSymbian::ensureExists(const QString& path, bool& created)
{
    CX_DEBUG_ENTER_FUNCTION();

    // Default to "not created" if errors are encountered.
    created = false;

    unsigned int placeHolder(0);
    TPtrC16 pathDesc(convertToTPtrC16(path));
    CX_DEBUG_SYMBIAN((_L("Checking [%S]"), &pathDesc));

    // Check if it already exists
    CX_DEBUG(("CxeFilenameGeneratorSymbian::ensureExists - check existence.."));
    int status = mFs.Att(pathDesc, placeHolder);
    CX_DEBUG(("CxeFilenameGeneratorSymbian::ensureExists - ..check existence"));

    if( status == KErrPathNotFound || status == KErrNotFound ) {
        // Create path if doesn't exist yet.
        CX_DEBUG(("CxeFilenameGeneratorSymbian::ensureExists - create path.."));
        status = mFs.MkDirAll(pathDesc);
        CX_DEBUG(("CxeFilenameGeneratorSymbian::ensureExists - ..create path"));
        // Set "created" flag only if successfull
        created = (status == KErrNone);
    }

    CX_DEBUG_EXIT_FUNCTION();
    return status;
}


// end of file
