//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//
// copyright            : (C) 2014 Eran Ifrah
// file name            : ASEnhancer.cpp
//
// -------------------------------------------------------------------------
// A
//              _____           _      _     _ _
//             /  __ \         | |    | |   (_) |
//             | /  \/ ___   __| | ___| |    _| |_ ___
//             | |    / _ \ / _  |/ _ \ |   | | __/ _ )
//             | \__/\ (_) | (_| |  __/ |___| | ||  __/
//              \____/\___/ \__,_|\___\_____/_|\__\___|
//
//                                                  F i l e
//
//    This program is free software; you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation; either version 2 of the License, or
//    (at your option) any later version.
//
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *   ASEnhancer.cpp
 *
 *   Copyright (C) 2006-2011 by Jim Pattee <jimp03@email.com>
 *   Copyright (C) 1998-2002 by Tal Davidson
 *   <http://www.gnu.org/licenses/lgpl-3.0.html>
 *
 *   This file is a part of Artistic Style - an indentation and
 *   reformatting tool for C, C++, C# and Java source files.
 *   <http://astyle.sourceforge.net>
 *
 *   Artistic Style 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.
 *
 *   Artistic Style 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 Artistic Style.  If not, see <http://www.gnu.org/licenses/>.
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 */

#include "astyle.h"
#include <iostream> // for cout

namespace astyle
{

/**
 * ASEnhancer constructor
 */
ASEnhancer::ASEnhancer() {}

/**
 * Destructor of ASEnhancer
 */
ASEnhancer::~ASEnhancer() {}

/**
 * initialize the ASEnhancer.
 *
 * init() is called each time an ASFormatter object is initialized.
 */
void ASEnhancer::init(int _fileType,
                      int _indentLength,
                      int _tabLength,
                      bool _useTabs,
                      bool _forceTab,
                      bool _caseIndent,
                      bool _preprocessorIndent,
                      bool _emptyLineFill)
{
    // formatting variables from ASFormatter and ASBeautifier
    ASBase::init(_fileType);
    indentLength = _indentLength;
    tabLength = _tabLength;
    useTabs = _useTabs;
    forceTab = _forceTab;
    caseIndent = _caseIndent;
    preprocessorIndent = _preprocessorIndent;
    emptyLineFill = _emptyLineFill;
    quoteChar = '\'';

    // unindent variables
    lineNumber = 0;
    bracketCount = 0;
    isInComment = false;
    isInQuote = false;
    switchDepth = 0;
    lookingForCaseBracket = false;
    unindentNextLine = false;
    shouldIndentLine = false;

    // switch struct and vector
    sw.switchBracketCount = 0;
    sw.unindentDepth = 0;
    sw.unindentCase = false;
    switchStack.clear();

    // other variables
    nextLineIsEventIndent = false;
    isInEventTable = false;
    nextLineIsDeclareIndent = false;
    isInDeclareSection = false;
}

/**
 * additional formatting for line of source code.
 * every line of source code in a source code file should be sent
 *     one after the other to this function.
 * indents event tables
 * unindents the case blocks
 *
 * @param line       the original formatted line will be updated if necessary.
 */
void ASEnhancer::enhance(string& line, bool isInPreprocessor, bool isInSQL)
{
    bool isSpecialChar = false; // is a backslash escape character
    shouldIndentLine = true;
    lineNumber++;

    // check for beginning of event table
    if(nextLineIsEventIndent) {
        isInEventTable = true;
        nextLineIsEventIndent = false;
    }

    // check for beginning of SQL declare section
    if(nextLineIsDeclareIndent) {
        isInDeclareSection = true;
        nextLineIsDeclareIndent = false;
    }

    if(line.length() == 0 && !isInEventTable && !isInDeclareSection && !emptyLineFill) return;

    // test for unindent on attached brackets
    if(unindentNextLine) {
        sw.unindentDepth++;
        sw.unindentCase = true;
        unindentNextLine = false;
    }

    // parse characters in the current line.

    for(size_t i = 0; i < line.length(); i++) {
        char ch = line[i];

        // bypass whitespace
        if(isWhiteSpace(ch)) continue;

        // handle special characters (i.e. backslash+character such as \n, \t, ...)
        if(isSpecialChar) {
            isSpecialChar = false;
            continue;
        }
        if(!(isInComment) && line.compare(i, 2, "\\\\") == 0) {
            i++;
            continue;
        }
        if(!(isInComment) && ch == '\\') {
            isSpecialChar = true;
            continue;
        }

        // handle quotes (such as 'x' and "Hello Dolly")
        if(!isInComment && (ch == '"' || ch == '\'')) {
            if(!isInQuote) {
                quoteChar = ch;
                isInQuote = true;
            } else if(quoteChar == ch) {
                isInQuote = false;
                continue;
            }
        }

        if(isInQuote) continue;

        // handle comments

        if(!(isInComment) && line.compare(i, 2, "//") == 0) {
            // check for windows line markers
            if(line.compare(i + 2, 1, "\xf0") > 0) lineNumber--;
            break; // finished with the line
        } else if(!(isInComment) && line.compare(i, 2, "/*") == 0) {
            isInComment = true;
            i++;
            continue;
        } else if((isInComment) && line.compare(i, 2, "*/") == 0) {
            isInComment = false;
            i++;
            continue;
        }

        if(isInComment) continue;

        // if we have reached this far then we are NOT in a comment or string of special characters

        if(line[i] == '{') bracketCount++;

        if(line[i] == '}') bracketCount--;

        bool isPotentialKeyword = isCharPotentialHeader(line, i);

        // ----------------  wxWidgets and MFC macros  ----------------------------------

        if(isPotentialKeyword) {
            if(findKeyword(line, i, "BEGIN_EVENT_TABLE") || findKeyword(line, i, "BEGIN_DISPATCH_MAP") ||
               findKeyword(line, i, "BEGIN_EVENT_MAP") || findKeyword(line, i, "BEGIN_MESSAGE_MAP") ||
               findKeyword(line, i, "BEGIN_PROPPAGEIDS")) {
                nextLineIsEventIndent = true;
                break;
            }
            if(findKeyword(line, i, "END_EVENT_TABLE") || findKeyword(line, i, "END_DISPATCH_MAP") ||
               findKeyword(line, i, "END_EVENT_MAP") || findKeyword(line, i, "END_MESSAGE_MAP") ||
               findKeyword(line, i, "END_PROPPAGEIDS")) {
                isInEventTable = false;
                break;
            }
        }

        // ----------------  process SQL  -----------------------------------------------

        if(isInSQL) {
            if(isBeginDeclareSectionSQL(line, i)) nextLineIsDeclareIndent = true;
            if(isEndDeclareSectionSQL(line, i)) isInDeclareSection = false;
            break;
        }

        // ----------------  process switch statements  ---------------------------------

        if(isPotentialKeyword && findKeyword(line, i, "switch")) {
            switchDepth++;
            switchStack.push_back(sw); // save current variables
            sw.switchBracketCount = 0;
            sw.unindentCase = false; // don't clear case until end of switch
            i += 5;                  // bypass switch statement
            continue;
        }

        // just want unindented case statements from this point

        if(caseIndent || switchDepth == 0 || (isInPreprocessor && !preprocessorIndent)) {
            // bypass the entire word
            if(isPotentialKeyword) {
                string name = getCurrentWord(line, i);
                i += name.length() - 1;
            }
            continue;
        }

        i = processSwitchBlock(line, i);

    } // end of for loop * end of for loop * end of for loop * end of for loop

    if(isInEventTable || isInDeclareSection) {
        if(line.length() == 0 || line[0] != '#') indentLine(line, 1);
    }

    if(shouldIndentLine && sw.unindentDepth > 0) unindentLine(line, sw.unindentDepth);
}

/**
 * find the colon following a 'case' statement
 *
 * @param line          a reference to the line.
 * @param i             the line index of the case statement.
 * @return              the line index of the colon.
 */
size_t ASEnhancer::findCaseColon(string& line, size_t caseIndex) const
{
    size_t i = caseIndex;
    bool isInQuote_ = false;
    char quoteChar_ = ' ';
    for(; i < line.length(); i++) {
        if(isInQuote_) {
            if(line[i] == '\\') {
                i++;
                continue;
            } else if(line[i] == quoteChar_) // check ending quote
            {
                isInQuote_ = false;
                quoteChar_ = ' ';
                continue;
            } else {
                continue; // must close quote before continuing
            }
        }
        if(line[i] == '\'' || line[i] == '\"') // check opening quote
        {
            isInQuote_ = true;
            quoteChar_ = line[i];
            continue;
        }
        if(line[i] == ':') {
            if((i + 1 < line.length()) && (line[i + 1] == ':'))
                i++; // bypass scope resolution operator
            else
                break; // found it
        }
    }
    return i;
}

/**
 * convert a force-tab indent to spaces
 *
 * @param line          a reference to the line that will be converted.
 */
void ASEnhancer::convertForceTabIndentToSpaces(string& line) const
{
    // replace tab indents with spaces
    for(size_t i = 0; i < line.length(); i++) {
        if(!isWhiteSpace(line[i])) break;
        if(line[i] == '\t') {
            line.erase(i, 1);
            line.insert(i, tabLength, ' ');
            i += tabLength - 1;
        }
    }
}

/**
 * convert a space indent to force-tab
 *
 * @param line          a reference to the line that will be converted.
 */
void ASEnhancer::convertSpaceIndentToForceTab(string& line) const
{
    assert(tabLength > 0);

    // replace leading spaces with tab indents
    size_t newSpaceIndentLength = line.find_first_not_of(" \t");
    size_t tabCount = newSpaceIndentLength / tabLength; // truncate extra spaces
    line.erase(0U, tabCount * tabLength);
    line.insert(0U, tabCount, '\t');
}

/**
* indent a line by a given number of tabsets
 *    by inserting leading whitespace to the line argument.
 *
 * @param line          a reference to the line to indent.
 * @param indent        the number of tabsets to insert.
 * @return              the number of characters inserted.
 */
int ASEnhancer::indentLine(string& line, int indent) const
{
    if(line.length() == 0 && !emptyLineFill) return 0;

    size_t charsToInsert;

    if(forceTab && indentLength != tabLength) {
        // replace tab indents with spaces
        convertForceTabIndentToSpaces(line);
        // insert the space indents
        charsToInsert = indent * indentLength;
        line.insert(0U, charsToInsert, ' ');
        // replace leading spaces with tab indents
        convertSpaceIndentToForceTab(line);
    } else if(useTabs) {
        charsToInsert = indent;
        line.insert(0U, charsToInsert, '\t');
    } else // spaces
    {
        charsToInsert = indent * indentLength;
        line.insert(0U, charsToInsert, ' ');
    }

    return charsToInsert;
}

/**
 * check for SQL "BEGIN DECLARE SECTION".
 * must compare case insensitive and allow any spacing between words.
 *
 * @param line          a reference to the line to indent.
 * @param index         the current line index.
 * @return              true if a hit.
 */
bool ASEnhancer::isBeginDeclareSectionSQL(string& line, size_t index) const
{
    string word;
    size_t hits = 0;
    size_t i;
    for(i = index; i < line.length(); i++) {
        i = line.find_first_not_of(" \t", i);
        if(i == string::npos) return false;
        if(line[i] == ';') break;
        if(!isCharPotentialHeader(line, i)) continue;
        word = getCurrentWord(line, i);
        for(size_t j = 0; j < word.length(); j++)
            word[j] = (char)toupper(word[j]);
        if(word == "EXEC" || word == "SQL") {
            i += word.length() - 1;
            continue;
        }
        if(word == "DECLARE" || word == "SECTION") {
            hits++;
            i += word.length() - 1;
            continue;
        }
        if(word == "BEGIN") {
            hits++;
            i += word.length() - 1;
            continue;
        }
        return false;
    }
    if(hits == 3) return true;
    return false;
}

/**
 * check for SQL "END DECLARE SECTION".
 * must compare case insensitive and allow any spacing between words.
 *
 * @param line          a reference to the line to indent.
 * @param index         the current line index.
 * @return              true if a hit.
 */
bool ASEnhancer::isEndDeclareSectionSQL(string& line, size_t index) const
{
    string word;
    size_t hits = 0;
    size_t i;
    for(i = index; i < line.length(); i++) {
        i = line.find_first_not_of(" \t", i);
        if(i == string::npos) return false;
        if(line[i] == ';') break;
        if(!isCharPotentialHeader(line, i)) continue;
        word = getCurrentWord(line, i);
        for(size_t j = 0; j < word.length(); j++)
            word[j] = (char)toupper(word[j]);
        if(word == "EXEC" || word == "SQL") {
            i += word.length() - 1;
            continue;
        }
        if(word == "DECLARE" || word == "SECTION") {
            hits++;
            i += word.length() - 1;
            continue;
        }
        if(word == "END") {
            hits++;
            i += word.length() - 1;
            continue;
        }
        return false;
    }
    if(hits == 3) return true;
    return false;
}

/**
 * check if a one-line bracket has been reached,
 * i.e. if the currently reached '{' character is closed
 * with a complimentry '}' elsewhere on the current line,
 *.
 * @return     false = one-line bracket has not been reached.
 *             true  = one-line bracket has been reached.
 */
bool ASEnhancer::isOneLineBlockReached(string& line, int startChar) const
{
    assert(line[startChar] == '{');

    bool isInComment_ = false;
    bool isInQuote_ = false;
    int _bracketCount = 1;
    int lineLength = line.length();
    char quoteChar_ = ' ';
    char ch = ' ';

    for(int i = startChar + 1; i < lineLength; ++i) {
        ch = line[i];

        if(isInComment_) {
            if(line.compare(i, 2, "*/") == 0) {
                isInComment_ = false;
                ++i;
            }
            continue;
        }

        if(ch == '\\') {
            ++i;
            continue;
        }

        if(isInQuote_) {
            if(ch == quoteChar_) isInQuote_ = false;
            continue;
        }

        if(ch == '"' || ch == '\'') {
            isInQuote_ = true;
            quoteChar_ = ch;
            continue;
        }

        if(line.compare(i, 2, "//") == 0) break;

        if(line.compare(i, 2, "/*") == 0) {
            isInComment_ = true;
            ++i;
            continue;
        }

        if(ch == '{')
            ++_bracketCount;
        else if(ch == '}')
            --_bracketCount;

        if(_bracketCount == 0) return true;
    }

    return false;
}

/**
 * process the character at the current index in a switch block.
 *
 * @param line          a reference to the line to indent.
 * @param index         the current line index.
 * @return              the new line index.
 */
size_t ASEnhancer::processSwitchBlock(string& line, size_t index)
{
    size_t i = index;
    bool isPotentialKeyword = isCharPotentialHeader(line, i);

    if(line[i] == '{') {
        sw.switchBracketCount++;
        if(lookingForCaseBracket) // if 1st after case statement
        {
            sw.unindentCase = true; // unindenting this case
            sw.unindentDepth++;
            lookingForCaseBracket = false; // not looking now
        }
        return i;
    }
    lookingForCaseBracket = false; // no opening bracket, don't indent

    if(line[i] == '}') {
        sw.switchBracketCount--;
        assert(sw.switchBracketCount <= bracketCount);
        if(sw.switchBracketCount == 0) // if end of switch statement
        {
            int lineUnindent = sw.unindentDepth;
            if(line.find_first_not_of(" \t") == i && !switchStack.empty())
                lineUnindent = switchStack[switchStack.size() - 1].unindentDepth;
            if(shouldIndentLine) {
                if(lineUnindent > 0) i -= unindentLine(line, lineUnindent);
                shouldIndentLine = false;
            }
            switchDepth--;
            sw = switchStack.back();
            switchStack.pop_back();
        }
        return i;
    }

    if(isPotentialKeyword && (findKeyword(line, i, "case") || findKeyword(line, i, "default"))) {
        if(sw.unindentCase) // if unindented last case
        {
            sw.unindentCase = false; // stop unindenting previous case
            sw.unindentDepth--;
        }

        i = findCaseColon(line, i);

        i++;
        for(; i < line.length(); i++) // bypass whitespace
        {
            if(!isWhiteSpace(line[i])) break;
        }
        if(i < line.length()) {
            if(line[i] == '{') {
                bracketCount++;
                sw.switchBracketCount++;
                if(!isOneLineBlockReached(line, i)) unindentNextLine = true;
                return i;
            }
        }
        lookingForCaseBracket = true;
        i--; // need to process this char
        return i;
    }
    if(isPotentialKeyword) {
        string name = getCurrentWord(line, i); // bypass the entire name
        i += name.length() - 1;
    }
    return i;
}

/**
 * unindent a line by a given number of tabsets
 *    by erasing the leading whitespace from the line argument.
 *
 * @param line          a reference to the line to unindent.
 * @param unindent      the number of tabsets to erase.
 * @return              the number of characters erased.
 */
int ASEnhancer::unindentLine(string& line, int unindent) const
{
    size_t whitespace = line.find_first_not_of(" \t");

    if(whitespace == string::npos)  // if line is blank
        whitespace = line.length(); // must remove padding, if any

    if(whitespace == 0) return 0;

    size_t charsToErase = 0;

    if(forceTab && indentLength != tabLength) {
        // replace tab indents with spaces
        convertForceTabIndentToSpaces(line);
        // remove the space indents
        size_t spaceIndentLength = line.find_first_not_of(" \t");
        charsToErase = unindent * indentLength;
        if(charsToErase <= spaceIndentLength)
            line.erase(0, charsToErase);
        else
            charsToErase = 0;
        // replace leading spaces with tab indents
        convertSpaceIndentToForceTab(line);
    } else if(useTabs) {
        charsToErase = unindent;
        if(charsToErase <= whitespace)
            line.erase(0, charsToErase);
        else
            charsToErase = 0;
    } else // spaces
    {
        charsToErase = unindent * indentLength;
        if(charsToErase <= whitespace)
            line.erase(0, charsToErase);
        else
            charsToErase = 0;
    }

    return charsToErase;
}

} // end namespace astyle
